diff --git a/e2e/nomostest/helm/helm.go b/e2e/nomostest/helm/helm.go
index 8b1f868d86..52a6ce9545 100644
--- a/e2e/nomostest/helm/helm.go
+++ b/e2e/nomostest/helm/helm.go
@@ -18,36 +18,34 @@ import (
"fmt"
"os"
"path/filepath"
- "strconv"
"strings"
- "time"
+ "github.com/ettle/strcase"
+ "github.com/google/uuid"
"kpt.dev/configsync/e2e"
"kpt.dev/configsync/e2e/nomostest"
+ "kpt.dev/configsync/e2e/nomostest/testlogger"
"kpt.dev/configsync/e2e/nomostest/testshell"
"sigs.k8s.io/kustomize/kyaml/copyutil"
+ "sigs.k8s.io/yaml"
)
-// PrivateARHelmRegistry is the registry URL to the private AR used for testing.
-// Cannot be assigned as a global variable due to GCPProject pointer, which gets
-// evaluated after initialization.
-func PrivateARHelmRegistry() string {
- return fmt.Sprintf("oci://us-docker.pkg.dev/%s/config-sync-test-ar-helm", *e2e.GCPProject)
-}
-
-// PrivateARHelmHost is the host name of the private AR used for testing
-var PrivateARHelmHost = "https://us-docker.pkg.dev"
-
// RemoteHelmChart represents a remote OCI-based helm chart
type RemoteHelmChart struct {
// Shell is a helper utility to execute shell commands in a test.
Shell *testshell.TestShell
- // Host is the host URL, e.g. https://us-docker.pkg.dev
- Host string
+ // Logger to write logs to
+ Logger *testlogger.TestLogger
+
+ // Project in which to store the chart image
+ Project string
- // Registry is the registry URL, e.g. oci://us-docker.pkg.dev/oss-prow-build-kpt-config-sync/config-sync-test-ar-helm
- Registry string
+ // Location to store the chart image
+ Location string
+
+ // RepositoryName in which to store the chart imageS
+ RepositoryName string
// ChartName is the name of the helm chart
ChartName string
@@ -55,59 +53,146 @@ type RemoteHelmChart struct {
// ChartVersion is the version of the helm chart
ChartVersion string
- // Dir is a local directory from which RemoteHelmChart will read, package, and push the chart from
- Dir string
+ // LocalChartPath is a local directory from which RemoteHelmChart will read, package, and push the chart from
+ LocalChartPath string
}
-// NewRemoteHelmChart creates a RemoteHelmChart
-func NewRemoteHelmChart(shell *testshell.TestShell, host, registry, dir, chartName, version string) *RemoteHelmChart {
- return &RemoteHelmChart{
- Shell: shell,
- Host: host,
- Registry: registry,
- Dir: dir,
- ChartName: chartName,
- ChartVersion: version,
+// CreateRepository uses gcloud to create the repository, if it doesn't exist.
+func (r *RemoteHelmChart) CreateRepository() error {
+ out, err := r.Shell.ExecWithDebug("gcloud", "artifacts", "repositories",
+ "describe", r.RepositoryName,
+ "--location", r.Location)
+ if err != nil {
+ if !strings.Contains(string(out), "NOT_FOUND") {
+ return fmt.Errorf("failed to describe image repository: %w", err)
+ }
+ // repository does not exist, continue with creation
+ } else {
+ // repository already exists, skip creation
+ return nil
+ }
+
+ r.Logger.Info("Creating image repository")
+ _, err = r.Shell.ExecWithDebug("gcloud", "artifacts", "repositories",
+ "create", r.RepositoryName,
+ "--repository-format", "docker",
+ "--location", r.Location)
+ if err != nil {
+ return fmt.Errorf("failed to create image repository: %w", err)
}
+ return nil
}
// RegistryLogin will log into the registry host specified by r.Host using local gcloud credentials
func (r *RemoteHelmChart) RegistryLogin() error {
var err error
authCmd := r.Shell.Command("gcloud", "auth", "print-access-token")
- loginCmd := r.Shell.Command("helm", "registry", "login", "-uoauth2accesstoken", "--password-stdin", r.Host)
+ loginCmd := r.Shell.Command("helm", "registry", "login",
+ "-uoauth2accesstoken", "--password-stdin",
+ fmt.Sprintf("https://%s", r.RegistryHost()))
loginCmd.Stdin, err = authCmd.StdoutPipe()
if err != nil {
- return fmt.Errorf("failed to setup command pipe: %v", err)
+ return fmt.Errorf("creating STDOUT pipe: %w", err)
}
if err := loginCmd.Start(); err != nil {
- return fmt.Errorf("failed to start login command: %v", err)
+ return fmt.Errorf("starting login command: %w", err)
}
if err := authCmd.Run(); err != nil {
- return fmt.Errorf("failed to run auth command: %v", err)
+ return fmt.Errorf("running print-access-token command: %w", err)
}
if err := loginCmd.Wait(); err != nil {
- return fmt.Errorf("failed to wait for login command: %v", err)
+ return fmt.Errorf("waiting for login command: %w", err)
+ }
+ return nil
+}
+
+// ConfigureAuthHelper configures the local docker client to use gcloud for
+// image registry authorization. Helm uses the docker config.
+func (r *RemoteHelmChart) ConfigureAuthHelper() error {
+ r.Logger.Info("Updating Docker config to use gcloud for auth")
+ if _, err := r.Shell.ExecWithDebug("gcloud", "auth", "configure-docker", r.RegistryHost()); err != nil {
+ return fmt.Errorf("failed to configure docker auth: %w", err)
}
return nil
}
+// RegistryHost returns the domain of the artifact registry
+func (r *RemoteHelmChart) RegistryHost() string {
+ return fmt.Sprintf("%s-docker.pkg.dev", r.Location)
+}
+
+// RepositoryAddress returns the domain and path to the chart repository
+func (r *RemoteHelmChart) RepositoryAddress() string {
+ return fmt.Sprintf("%s/%s/%s", r.RegistryHost(), r.Project, r.RepositoryName)
+}
+
+// RepositoryOCI returns the repository address with the oci:// scheme prefix.
+func (r *RemoteHelmChart) RepositoryOCI() string {
+ return fmt.Sprintf("oci://%s", r.RepositoryAddress())
+}
+
+// ChartAddress returns the domain and path to the chart image
+func (r *RemoteHelmChart) ChartAddress() string {
+ return fmt.Sprintf("%s/%s", r.RepositoryAddress(), r.ChartName)
+}
+
// CopyChartFromLocal accepts a local path to a helm chart and recursively copies it to r.Dir, modifying
// the name of the copied chart from its original name to r.ChartName
-func (r *RemoteHelmChart) CopyChartFromLocal(chartPath, originalChartName string) error {
- if err := copyutil.CopyDir(chartPath, r.Dir); err != nil {
- return fmt.Errorf("failed to copy helm chart: %v", err)
+func (r *RemoteHelmChart) CopyChartFromLocal(chartPath string) error {
+ r.Logger.Infof("Copying helm chart from test artifacts: %s", chartPath)
+ if err := os.MkdirAll(r.LocalChartPath, os.ModePerm); err != nil {
+ return fmt.Errorf("creating helm chart directory: %v", err)
+ }
+ if err := copyutil.CopyDir(chartPath, r.LocalChartPath); err != nil {
+ return fmt.Errorf("copying helm chart: %v", err)
}
- if err := findAndReplaceInFile(filepath.Join(r.Dir, "Chart.yaml"), fmt.Sprintf("name: %s", originalChartName), fmt.Sprintf("name: %s", r.ChartName)); err != nil {
- return fmt.Errorf("failed to rename helm chart: %v", err)
+ r.Logger.Infof("Updating helm chart name & version: %s:%s", r.ChartName, r.ChartVersion)
+ chartFilePath := filepath.Join(r.LocalChartPath, "Chart.yaml")
+ err := updateYAMLFile(chartFilePath, func(chartMap map[string]interface{}) error {
+ chartMap["name"] = r.ChartName
+ chartMap["version"] = r.ChartVersion
+ return nil
+ })
+ if err != nil {
+ return fmt.Errorf("updating Chart.yaml: %v", err)
}
return nil
}
-// UpdateVersion updates the local version of the helm chart to version
+func updateYAMLFile(name string, updateFn func(map[string]interface{}) error) error {
+ chartBytes, err := os.ReadFile(name)
+ if err != nil {
+ return fmt.Errorf("reading file: %s: %w", name, err)
+ }
+ var chartManifest map[string]interface{}
+ if err := yaml.Unmarshal(chartBytes, &chartManifest); err != nil {
+ return fmt.Errorf("parsing yaml file: %s: %w", name, err)
+ }
+ if err := updateFn(chartManifest); err != nil {
+ return fmt.Errorf("updating yaml map for %s: %w", name, err)
+ }
+ chartBytes, err = yaml.Marshal(chartManifest)
+ if err != nil {
+ return fmt.Errorf("formatting yaml for %s: %w", name, err)
+ }
+ if err := os.WriteFile(name, chartBytes, os.ModePerm); err != nil {
+ return fmt.Errorf("writing file: %s: %w", name, err)
+ }
+ return nil
+}
+
+// UpdateVersion updates the local version of the helm chart to the specified
+// version with a timestamp suffix
func (r *RemoteHelmChart) UpdateVersion(version string) error {
- if err := findAndReplaceInFile(filepath.Join(r.Dir, "Chart.yaml"), fmt.Sprintf("version: %s", r.ChartVersion), fmt.Sprintf("version: %s", version)); err != nil {
- return fmt.Errorf("failed to update helm chart version: %v", err)
+ version = generateChartVersion(version)
+ r.Logger.Infof("Updating helm chart version to %q", version)
+ chartFilePath := filepath.Join(r.LocalChartPath, "Chart.yaml")
+ err := updateYAMLFile(chartFilePath, func(chartMap map[string]interface{}) error {
+ chartMap["version"] = version
+ return nil
+ })
+ if err != nil {
+ return fmt.Errorf("updating Chart.yaml: %v", err)
}
r.ChartVersion = version
return nil
@@ -115,77 +200,102 @@ func (r *RemoteHelmChart) UpdateVersion(version string) error {
// Push will package and push the helm chart located at r.Dir to the remote registry.
func (r *RemoteHelmChart) Push() error {
- if _, err := r.Shell.Helm("package", r.Dir, "--destination", r.Dir); err != nil {
- return fmt.Errorf("failed to package helm chart: %v", err)
+ r.Logger.Infof("Packaging helm chart: %s:%s", r.ChartName, r.ChartVersion)
+ parentPath := filepath.Dir(r.LocalChartPath)
+ if _, err := r.Shell.Helm("package", r.LocalChartPath, "--destination", parentPath+string(filepath.Separator)); err != nil {
+ return fmt.Errorf("packaging helm chart: %w", err)
}
- chartFile := filepath.Join(r.Dir, fmt.Sprintf("%s-%s.tgz", r.ChartName, r.ChartVersion))
- if out, err := r.Shell.Helm("push", chartFile, r.Registry); err != nil {
- return fmt.Errorf("failed to run `helm push`: %s; %v", string(out), err)
+ r.Logger.Infof("Pushing helm chart: %s:%s", r.ChartName, r.ChartVersion)
+ chartFile := filepath.Join(parentPath, fmt.Sprintf("%s-%s.tgz", r.ChartName, r.ChartVersion))
+ if _, err := r.Shell.Helm("push", chartFile, r.RepositoryOCI()); err != nil {
+ return fmt.Errorf("pushing helm chart: %w", err)
+ }
+ if err := os.Remove(chartFile); err != nil {
+ return fmt.Errorf("deleting local helm chart package: %w", err)
}
return nil
}
-// PushHelmChart pushes a new helm chart for use during an e2e test. Returns a reference to the RemoteHelmChart object
-// and any errors that are encountered.
-func PushHelmChart(nt *nomostest.NT, helmchart, version string) (*RemoteHelmChart, error) {
- nt.T.Log("Push helm chart to the artifact registry")
+// Delete the package from the remote registry, including all versions and tags.
+func (r *RemoteHelmChart) Delete() error {
+ r.Logger.Infof("Deleting helm chart: %s", r.ChartName)
+ if _, err := r.Shell.ExecWithDebug("gcloud", "artifacts", "docker", "images", "delete", r.ChartAddress(), "--delete-tags"); err != nil {
+ return fmt.Errorf("deleting helm chart image from registry: %w", err)
+ }
+ return nil
+}
- chartName := generateChartName(helmchart, nt.ClusterName)
+// PushHelmChart pushes a new helm chart for use during an e2e test.
+// Returns a reference to the RemoteHelmChart object and any errors.
+func PushHelmChart(nt *nomostest.NT, chartName, chartVersion string) (*RemoteHelmChart, error) {
+ if chartName == "" {
+ return nil, fmt.Errorf("chart name must not be empty")
+ }
+ if chartVersion == "" {
+ return nil, fmt.Errorf("chart version must not be empty")
+ }
+ chart := &RemoteHelmChart{
+ Shell: nt.Shell,
+ Logger: nt.Logger,
+ Project: *e2e.GCPProject,
+ Location: "us", // store redundantly across regions in the US.
+ // Use cluster name to avoid overlap between images in parallel test runs.
+ RepositoryName: fmt.Sprintf("config-sync-e2e-test--%s", nt.ClusterName),
+ // Use chart name to avoid overlap between multiple charts in the same test.
+ LocalChartPath: filepath.Join(nt.TmpDir, chartName),
+ // Use test name and timestamp to avoid overlap between sequential test runs.
+ ChartName: generateChartName(chartName, strcase.ToKebab(nt.T.Name())),
+ ChartVersion: generateChartVersion(chartVersion),
+ }
nt.T.Cleanup(func() {
- if err := cleanHelmImages(nt, chartName); err != nil {
+ if err := chart.Delete(); err != nil {
nt.T.Errorf(err.Error())
}
})
-
- remoteHelmChart := NewRemoteHelmChart(nt.Shell, PrivateARHelmHost, PrivateARHelmRegistry(), nt.TmpDir, chartName, version)
- err := remoteHelmChart.CopyChartFromLocal(fmt.Sprintf("../testdata/helm-charts/%s", helmchart), helmchart)
- if err != nil {
+ artifactPath := fmt.Sprintf("../testdata/helm-charts/%s", chartName)
+ if err := chart.CopyChartFromLocal(artifactPath); err != nil {
return nil, err
}
- if err := remoteHelmChart.RegistryLogin(); err != nil {
- return nil, fmt.Errorf("failed to login to the helm registry: %v", err)
+ if err := chart.CreateRepository(); err != nil {
+ return nil, err
}
- if err := remoteHelmChart.Push(); err != nil {
+ // TODO: Figure out why gcloud auth doesn't always work with helm push (401 Unauthorized) on new repositories
+ // if err := chart.ConfigureAuthHelper(); err != nil {
+ // return nil, err
+ // }
+ if err := chart.RegistryLogin(); err != nil {
return nil, err
}
-
- return remoteHelmChart, nil
+ if err := chart.Push(); err != nil {
+ return nil, err
+ }
+ return chart, nil
}
-// creates a chart name from current project id, cluster name, and timestamp
-func generateChartName(helmchart, clusterName string) string {
- chartName := fmt.Sprintf("%s-%s-%s", helmchart, clusterName, timestampAsString())
- if len(chartName) > 50 {
- // the chartName + releaseName is used as the metadata.name of resources in the coredns helm chart, so we must trim this down
- // to keep it under the k8s length limit
- chartName = chartName[len(chartName)-50:]
+// creates a chart name from the chart name, test name, and timestamp.
+// Result will be no more than 40 characters and can function as a k8s metadata.name.
+// Chart name and version must be less than 63 characters combined.
+func generateChartName(chartName, testName string) string {
+ if len(chartName) > 20 {
+ chartName = chartName[:20]
chartName = strings.Trim(chartName, "-")
}
- return chartName
-}
-
-// removes helm charts created during e2e testing
-func cleanHelmImages(nt *nomostest.NT, chartName string) error {
- if out, err := nt.Shell.Command("gcloud", "artifacts", "docker", "images", "delete", fmt.Sprintf("us-docker.pkg.dev/%s/config-sync-test-ar-helm/%s", *e2e.GCPProject, chartName), "--delete-tags").CombinedOutput(); err != nil {
- return fmt.Errorf("failed to cleanup helm chart image from registry: %s; %v", string(out), err)
+ chartName = fmt.Sprintf("%s-%s", chartName, testName)
+ if len(chartName) > 40 {
+ chartName = chartName[:40]
+ chartName = strings.Trim(chartName, "-")
}
- return nil
+ return chartName
}
-// finds and replaces particular text string in a file
-func findAndReplaceInFile(path, old, new string) error {
- oldFile, err := os.ReadFile(path)
- if err != nil {
- return fmt.Errorf("could not read file: %v", err)
- }
- err = os.WriteFile(path, []byte(strings.ReplaceAll(string(oldFile), old, new)), 0644)
- if err != nil {
- return fmt.Errorf("could not write to file: %v", err)
+// generateChartVersion returns the version with a random 8 character suffix.
+// Result will be no more than 20 characters and can function as a k8s metadata.name.
+// Chart name and version must be less than 63 characters combined.
+func generateChartVersion(chartVersion string) string {
+ if len(chartVersion) > 12 {
+ chartVersion = chartVersion[:12]
+ chartVersion = strings.Trim(strings.Trim(chartVersion, "-"), ".")
}
- return nil
-}
-
-// returns the current unix timestamp as a string
-func timestampAsString() string {
- return strconv.FormatInt(time.Now().Unix(), 10)
+ return fmt.Sprintf("%s-%s", chartVersion,
+ strings.ReplaceAll(uuid.NewString(), "-", "")[:7])
}
diff --git a/e2e/nomostest/name.go b/e2e/nomostest/name.go
index c5dd7ed995..fb528f15ea 100644
--- a/e2e/nomostest/name.go
+++ b/e2e/nomostest/name.go
@@ -18,17 +18,13 @@ import (
"crypto/sha1"
"encoding/hex"
"fmt"
- "regexp"
"strings"
+ "github.com/ettle/strcase"
"k8s.io/apimachinery/pkg/util/validation"
"kpt.dev/configsync/e2e/nomostest/testing"
)
-// re splits strings at word boundaries. Test names always begin with "Test",
-// so we know we aren't missing any of the text.
-var re = regexp.MustCompile(`[A-Z][^A-Z]*`)
-
// TestClusterName returns the name of the test cluster.
func TestClusterName(t testing.NTB) string {
t.Helper()
@@ -58,14 +54,9 @@ func testDirName(t testing.NTB) string {
t.Helper()
n := t.Name()
- // Capital letters are forbidden in Kind cluster names, so convert to
- // kebab-case.
- words := re.FindAllString(n, -1)
- for i, w := range words {
- words[i] = strings.ToLower(w)
- }
-
- n = strings.Join(words, "-")
+ // Capital letters are forbidden in Kind cluster names
+ n = strcase.ToKebab(n)
+ // Slashes are forbidden in file paths.
n = strings.ReplaceAll(n, "/", "--")
return n
}
diff --git a/e2e/testcases/csr_auth_test.go b/e2e/testcases/csr_auth_test.go
index bd3e20000f..6863594e9a 100644
--- a/e2e/testcases/csr_auth_test.go
+++ b/e2e/testcases/csr_auth_test.go
@@ -314,13 +314,15 @@ func testWorkloadIdentity(t *testing.T, testSpec workloadIdentityTestSpec) {
// For helm charts, we need to push the chart to the AR before configuring the RootSync
if testSpec.sourceType == v1beta1.HelmSource {
- remoteHelmChart, err := helm.PushHelmChart(nt, privateCoreDNSHelmChart, privateCoreDNSHelmChartVersion)
+ chart, err := helm.PushHelmChart(nt, testSpec.sourceChart, testSpec.sourceVersion)
if err != nil {
nt.T.Fatalf("failed to push helm chart: %v", err)
}
- testSpec.sourceChart = remoteHelmChart.ChartName
- testSpec.rootCommitFn = helmChartVersion(testSpec.sourceVersion)
+ testSpec.sourceRepo = chart.RepositoryOCI()
+ testSpec.sourceChart = chart.ChartName
+ testSpec.sourceVersion = chart.ChartVersion
+ testSpec.rootCommitFn = helmChartVersion(chart.ChartVersion)
}
// Reuse the RootSync instead of creating a new one so that testing resources can be cleaned up after the test.
diff --git a/e2e/testcases/helm_sync_test.go b/e2e/testcases/helm_sync_test.go
index 00b7d8de7d..3a823b1680 100644
--- a/e2e/testcases/helm_sync_test.go
+++ b/e2e/testcases/helm_sync_test.go
@@ -23,6 +23,7 @@ import (
"k8s.io/apimachinery/pkg/api/resource"
"k8s.io/apimachinery/pkg/types"
"k8s.io/utils/pointer"
+ "kpt.dev/configsync/e2e"
"kpt.dev/configsync/e2e/nomostest"
"kpt.dev/configsync/e2e/nomostest/gitproviders"
"kpt.dev/configsync/e2e/nomostest/helm"
@@ -397,16 +398,16 @@ func TestHelmDefaultNamespace(t *testing.T) {
rs := fake.RootSyncObjectV1Beta1(configsync.RootSyncName)
- remoteHelmChart, err := helm.PushHelmChart(nt, privateSimpleHelmChart, privateSimpleHelmChartVersion)
+ chart, err := helm.PushHelmChart(nt, privateSimpleHelmChart, privateSimpleHelmChartVersion)
if err != nil {
nt.T.Fatalf("failed to push helm chart: %v", err)
}
nt.T.Log("Update RootSync to sync from a private Artifact Registry")
nt.MustMergePatch(rs, fmt.Sprintf(`{"spec": {"sourceType": "%s", "git": null, "helm": {"repo": "%s", "chart": "%s", "version": "%s", "auth": "gcpserviceaccount", "gcpServiceAccountEmail": "%s", "namespace": "", "deployNamespace": ""}}}`,
- v1beta1.HelmSource, helm.PrivateARHelmRegistry(), remoteHelmChart.ChartName, privateSimpleHelmChartVersion, gsaARReaderEmail()))
- err = nt.WatchForAllSyncs(nomostest.WithRootSha1Func(helmChartVersion(privateSimpleHelmChartVersion)),
- nomostest.WithSyncDirectoryMap(map[types.NamespacedName]string{nomostest.DefaultRootRepoNamespacedName: remoteHelmChart.ChartName}))
+ v1beta1.HelmSource, chart.RepositoryOCI(), chart.ChartName, chart.ChartVersion, gsaARReaderEmail()))
+ err = nt.WatchForAllSyncs(nomostest.WithRootSha1Func(helmChartVersion(chart.ChartVersion)),
+ nomostest.WithSyncDirectoryMap(map[types.NamespacedName]string{nomostest.DefaultRootRepoNamespacedName: chart.ChartName}))
if err != nil {
nt.T.Fatal(err)
}
@@ -442,41 +443,41 @@ func TestHelmLatestVersion(t *testing.T) {
)
rs := fake.RootSyncObjectV1Beta1(configsync.RootSyncName)
- remoteHelmChart, err := helm.PushHelmChart(nt, privateSimpleHelmChart, privateSimpleHelmChartVersion)
+ chart, err := helm.PushHelmChart(nt, privateSimpleHelmChart, privateSimpleHelmChartVersion)
if err != nil {
nt.T.Fatalf("failed to push helm chart: %v", err)
}
nt.T.Log("Update RootSync to sync from a private Artifact Registry")
nt.MustMergePatch(rs, fmt.Sprintf(`{"spec": {"sourceType": "%s", "helm": {"chart": "%s", "repo": "%s", "version": "", "period": "5s", "auth": "gcpserviceaccount", "gcpServiceAccountEmail": "%s", "deployNamespace": "simple"}, "git": null}}`,
- v1beta1.HelmSource, remoteHelmChart.ChartName, helm.PrivateARHelmRegistry(), gsaARReaderEmail()))
+ v1beta1.HelmSource, chart.ChartName, chart.RepositoryOCI(), gsaARReaderEmail()))
if err = nt.Watcher.WatchObject(kinds.Deployment(), "deploy-default", "simple",
- []testpredicates.Predicate{testpredicates.HasLabel("version", privateSimpleHelmChartVersion)}); err != nil {
+ []testpredicates.Predicate{testpredicates.HasLabel("version", chart.ChartVersion)}); err != nil {
nt.T.Error(err)
}
// helm-sync automatically detects and updates to the new helm chart version
newVersion := "2.5.9"
- if err := remoteHelmChart.UpdateVersion(newVersion); err != nil {
+ if err := chart.UpdateVersion(newVersion); err != nil {
nt.T.Fatal(err)
}
- if err := remoteHelmChart.Push(); err != nil {
+ if err := chart.Push(); err != nil {
nt.T.Fatal("failed to push helm chart update: %v", err)
}
if err = nt.Watcher.WatchObject(kinds.Deployment(), "deploy-default", "simple",
- []testpredicates.Predicate{testpredicates.HasLabel("version", newVersion)}); err != nil {
+ []testpredicates.Predicate{testpredicates.HasLabel("version", chart.ChartVersion)}); err != nil {
nt.T.Error(err)
}
newVersion = "3.0.0"
- if err := remoteHelmChart.UpdateVersion(newVersion); err != nil {
+ if err := chart.UpdateVersion(newVersion); err != nil {
nt.T.Fatal(err)
}
- if err := remoteHelmChart.Push(); err != nil {
+ if err := chart.Push(); err != nil {
nt.T.Fatal("failed to push helm chart update: %v", err)
}
if err = nt.Watcher.WatchObject(kinds.Deployment(), "deploy-default", "simple",
- []testpredicates.Predicate{testpredicates.HasLabel("version", newVersion)}); err != nil {
+ []testpredicates.Predicate{testpredicates.HasLabel("version", chart.ChartVersion)}); err != nil {
nt.T.Error(err)
}
}
@@ -523,27 +524,27 @@ func TestHelmNamespaceRepo(t *testing.T) {
nt.Must(nt.RootRepos[configsync.RootSyncName].CommitAndPush("Update RepoSync to sync from a public Helm Chart with cluster-scoped type"))
nt.WaitForRepoSyncSourceError(repoSyncNN.Namespace, repoSyncNN.Name, nonhierarchical.BadScopeErrCode, "must be Namespace-scoped type")
- remoteHelmChart, err := helm.PushHelmChart(nt, privateNSHelmChart, privateNSHelmChartVersion)
+ chart, err := helm.PushHelmChart(nt, privateNSHelmChart, privateNSHelmChartVersion)
if err != nil {
nt.T.Fatalf("failed to push helm chart: %v", err)
}
nt.T.Log("Update RepoSync to sync from a private Artifact Registry")
rs.Spec.Helm = &v1beta1.HelmRepoSync{HelmBase: v1beta1.HelmBase{
- Repo: helm.PrivateARHelmRegistry(),
- Chart: remoteHelmChart.ChartName,
+ Repo: chart.RepositoryOCI(),
+ Chart: chart.ChartName,
Auth: configsync.AuthGCPServiceAccount,
GCPServiceAccountEmail: gsaARReaderEmail(),
- Version: privateNSHelmChartVersion,
+ Version: chart.ChartVersion,
ReleaseName: "test",
}}
nt.Must(nt.RootRepos[configsync.RootSyncName].Add(nomostest.StructuredNSPath(repoSyncNN.Namespace, repoSyncNN.Name), rs))
nt.Must(nt.RootRepos[configsync.RootSyncName].CommitAndPush("Update RepoSync to sync from a private Helm Chart without cluster scoped resources"))
- err = nt.WatchForAllSyncs(nomostest.WithRepoSha1Func(helmChartVersion(privateNSHelmChartVersion)), nomostest.WithSyncDirectoryMap(map[types.NamespacedName]string{repoSyncNN: remoteHelmChart.ChartName}))
+ err = nt.WatchForAllSyncs(nomostest.WithRepoSha1Func(helmChartVersion(chart.ChartVersion)), nomostest.WithSyncDirectoryMap(map[types.NamespacedName]string{repoSyncNN: chart.ChartName}))
if err != nil {
nt.T.Fatal(err)
}
- if err := nt.Validate(rs.Spec.Helm.ReleaseName+"-"+remoteHelmChart.ChartName, testNs, &appsv1.Deployment{}); err != nil {
+ if err := nt.Validate(rs.Spec.Helm.ReleaseName+"-"+chart.ChartName, testNs, &appsv1.Deployment{}); err != nil {
nt.T.Error(err)
}
}
@@ -561,7 +562,7 @@ func TestHelmConfigMapNamespaceRepo(t *testing.T) {
rs := nomostest.RepoSyncObjectV1Beta1FromNonRootRepo(nt, repoSyncNN)
cmName := "helm-cm-ns-repo-1"
- remoteHelmChart, err := helm.PushHelmChart(nt, privateNSHelmChart, privateNSHelmChartVersion)
+ chart, err := helm.PushHelmChart(nt, privateNSHelmChart, privateNSHelmChartVersion)
if err != nil {
nt.T.Fatalf("failed to push helm chart: %v", err)
}
@@ -569,11 +570,11 @@ func TestHelmConfigMapNamespaceRepo(t *testing.T) {
nt.T.Log("Update RepoSync to sync from a private Artifact Registry")
rs.Spec.SourceType = string(v1beta1.HelmSource)
rs.Spec.Helm = &v1beta1.HelmRepoSync{HelmBase: v1beta1.HelmBase{
- Repo: helm.PrivateARHelmRegistry(),
- Chart: remoteHelmChart.ChartName,
+ Repo: chart.RepositoryOCI(),
+ Chart: chart.ChartName,
Auth: configsync.AuthGCPServiceAccount,
GCPServiceAccountEmail: gsaARReaderEmail(),
- Version: privateNSHelmChartVersion,
+ Version: chart.ChartVersion,
ReleaseName: "test",
ValuesFileRefs: []v1beta1.ValuesFileRef{{Name: cmName, DataKey: "foo.yaml"}},
}}
@@ -643,11 +644,13 @@ func TestHelmConfigMapNamespaceRepo(t *testing.T) {
nt.Must(nt.RootRepos[configsync.RootSyncName].Add(nomostest.StructuredNSPath(repoSyncNN.Namespace, repoSyncNN.Name), rs))
nt.Must(nt.RootRepos[configsync.RootSyncName].CommitAndPush("Update RepoSync to reference new ConfigMap"))
- err = nt.WatchForAllSyncs(nomostest.WithRepoSha1Func(helmChartVersion(privateNSHelmChartVersion)), nomostest.WithSyncDirectoryMap(map[types.NamespacedName]string{repoSyncNN: remoteHelmChart.ChartName}))
+ err = nt.WatchForAllSyncs(
+ nomostest.WithRepoSha1Func(helmChartVersion(chart.ChartVersion)),
+ nomostest.WithSyncDirectoryMap(map[types.NamespacedName]string{repoSyncNN: chart.ChartName}))
if err != nil {
nt.T.Fatal(err)
}
- if err := nt.Validate(rs.Spec.Helm.ReleaseName+"-"+remoteHelmChart.ChartName, testNs, &appsv1.Deployment{},
+ if err := nt.Validate(rs.Spec.Helm.ReleaseName+"-"+chart.ChartName, testNs, &appsv1.Deployment{},
testpredicates.HasLabel("labelsTest", "foo")); err != nil {
nt.T.Fatal(err)
}
@@ -671,7 +674,6 @@ func TestHelmARFleetWISameProject(t *testing.T) {
testWorkloadIdentity(t, workloadIdentityTestSpec{
fleetWITest: true,
crossProject: false,
- sourceRepo: helm.PrivateARHelmRegistry(),
sourceVersion: privateCoreDNSHelmChartVersion,
sourceChart: privateCoreDNSHelmChart,
sourceType: v1beta1.HelmSource,
@@ -699,7 +701,6 @@ func TestHelmARFleetWIDifferentProject(t *testing.T) {
testWorkloadIdentity(t, workloadIdentityTestSpec{
fleetWITest: true,
crossProject: true,
- sourceRepo: helm.PrivateARHelmRegistry(),
sourceVersion: privateCoreDNSHelmChartVersion,
sourceChart: privateCoreDNSHelmChart,
sourceType: v1beta1.HelmSource,
@@ -726,7 +727,6 @@ func TestHelmARGKEWorkloadIdentity(t *testing.T) {
testWorkloadIdentity(t, workloadIdentityTestSpec{
fleetWITest: false,
crossProject: false,
- sourceRepo: helm.PrivateARHelmRegistry(),
sourceVersion: privateCoreDNSHelmChartVersion,
sourceChart: privateCoreDNSHelmChart,
sourceType: v1beta1.HelmSource,
@@ -744,7 +744,7 @@ func TestHelmGCENode(t *testing.T) {
nt := nomostest.New(t, nomostesting.SyncSource, ntopts.Unstructured,
ntopts.RequireGKE(t), ntopts.GCENodeTest)
- remoteHelmChart, err := helm.PushHelmChart(nt, privateCoreDNSHelmChart, privateCoreDNSHelmChartVersion)
+ chart, err := helm.PushHelmChart(nt, privateCoreDNSHelmChart, privateCoreDNSHelmChartVersion)
if err != nil {
nt.T.Fatalf("failed to push helm chart: %v", err)
}
@@ -752,13 +752,14 @@ func TestHelmGCENode(t *testing.T) {
rs := fake.RootSyncObjectV1Beta1(configsync.RootSyncName)
nt.T.Log("Update RootSync to sync from a private Artifact Registry")
nt.MustMergePatch(rs, fmt.Sprintf(`{"spec": {"sourceType": "%s", "helm": {"repo": "%s", "chart": "%s", "auth": "gcenode", "version": "%s", "releaseName": "my-coredns", "namespace": "coredns"}, "git": null}}`,
- v1beta1.HelmSource, helm.PrivateARHelmRegistry(), remoteHelmChart.ChartName, privateCoreDNSHelmChartVersion))
- err = nt.WatchForAllSyncs(nomostest.WithRootSha1Func(helmChartVersion(privateCoreDNSHelmChartVersion)),
- nomostest.WithSyncDirectoryMap(map[types.NamespacedName]string{nomostest.DefaultRootRepoNamespacedName: remoteHelmChart.ChartName}))
+ v1beta1.HelmSource, chart.RepositoryOCI(), chart.ChartName, chart.ChartVersion))
+ err = nt.WatchForAllSyncs(
+ nomostest.WithRootSha1Func(helmChartVersion(chart.ChartVersion)),
+ nomostest.WithSyncDirectoryMap(map[types.NamespacedName]string{nomostest.DefaultRootRepoNamespacedName: chart.ChartName}))
if err != nil {
nt.T.Fatal(err)
}
- if err := nt.Validate(fmt.Sprintf("my-coredns-%s", remoteHelmChart.ChartName), "coredns", &appsv1.Deployment{},
+ if err := nt.Validate(fmt.Sprintf("my-coredns-%s", chart.ChartName), "coredns", &appsv1.Deployment{},
testpredicates.DeploymentContainerPullPolicyEquals("coredns", "IfNotPresent")); err != nil {
nt.T.Error(err)
}
@@ -791,24 +792,96 @@ func TestHelmARTokenAuth(t *testing.T) {
nt.MustKubectl("delete", "secret", "foo", "-n", v1.NSConfigManagementSystem, "--ignore-not-found")
})
- remoteHelmChart, err := helm.PushHelmChart(nt, privateCoreDNSHelmChart, privateCoreDNSHelmChartVersion)
+ chart, err := helm.PushHelmChart(nt, privateCoreDNSHelmChart, privateCoreDNSHelmChartVersion)
if err != nil {
nt.T.Fatalf("failed to push helm chart: %v", err)
}
nt.T.Log("Update RootSync to sync from a private Artifact Registry")
nt.MustMergePatch(rs, fmt.Sprintf(`{"spec": {"sourceType": "%s", "git": null, "helm": {"repo": "%s", "chart": "%s", "auth": "token", "version": "%s", "releaseName": "my-coredns", "namespace": "coredns", "secretRef": {"name" : "foo"}}}}`,
- v1beta1.HelmSource, helm.PrivateARHelmRegistry(), remoteHelmChart.ChartName, privateCoreDNSHelmChartVersion))
- err = nt.WatchForAllSyncs(nomostest.WithRootSha1Func(helmChartVersion(privateCoreDNSHelmChartVersion)),
- nomostest.WithSyncDirectoryMap(map[types.NamespacedName]string{nomostest.DefaultRootRepoNamespacedName: remoteHelmChart.ChartName}))
+ v1beta1.HelmSource, chart.RepositoryOCI(), chart.ChartName, chart.ChartVersion))
+ err = nt.WatchForAllSyncs(
+ nomostest.WithRootSha1Func(helmChartVersion(chart.ChartVersion)),
+ nomostest.WithSyncDirectoryMap(map[types.NamespacedName]string{nomostest.DefaultRootRepoNamespacedName: chart.ChartName}))
if err != nil {
nt.T.Fatal(err)
}
- if err := nt.Validate(fmt.Sprintf("my-coredns-%s", remoteHelmChart.ChartName), "coredns", &appsv1.Deployment{}); err != nil {
+ if err := nt.Validate(fmt.Sprintf("my-coredns-%s", chart.ChartName), "coredns", &appsv1.Deployment{}); err != nil {
nt.T.Error(err)
}
}
+// TestHelmEmptyChart verifies Config Sync can apply an empty Helm chart.
+//
+// Requirements:
+// 1. Helm auth https://cloud.google.com/artifact-registry/docs/helm/authentication
+// 2. gcloud auth login OR gcloud auth activate-service-account ACCOUNT --key-file=KEY-FILE
+// 3. Artifact Registry repo: us-docker.pkg.dev/${GCP_PROJECT}/config-sync-test-private
+// 4. GKE cluster with Workload Identity
+// 5. Google Service Account: e2e-test-ar-reader@${GCP_PROJECT}.iam.gserviceaccount.com
+// 6. IAM for the GSA to read from the Artifact Registry repo
+// 7. IAM for the test runner to write to Artifact Registry repo
+// 8. gcloud & helm
+func TestHelmEmptyChart(t *testing.T) {
+ nt := nomostest.New(t,
+ nomostesting.SyncSource,
+ ntopts.Unstructured,
+ ntopts.RequireGKE(t),
+ )
+
+ // Verify workload identity is enabled on the cluster
+ expectedPool := fmt.Sprintf("%s.svc.id.goog", *e2e.GCPProject)
+ if workloadPool, err := getWorkloadPool(nt); err != nil {
+ nt.T.Fatal(err)
+ } else if workloadPool != expectedPool {
+ nt.T.Fatalf("expected workloadPool %s but got %s", expectedPool, workloadPool)
+ }
+
+ // Validate that the GCP Service Account exists.
+ // GSA is used for CS to read the Helm Chart from Artifact Registry.
+ // The local test user will also need to be able to push to Artifact Registry.
+ gsaEmail := gsaARReaderEmail()
+ if _, err := describeGCPServiceAccount(nt, gsaEmail, *e2e.GCPProject); err != nil {
+ nt.T.Fatalf("failed to get service account for workload identity: %v", err)
+ }
+
+ chart, err := helm.PushHelmChart(nt, "empty", "v1.0.0")
+ if err != nil {
+ nt.T.Fatalf("failed to push helm chart: %v", err)
+ }
+
+ nt.T.Logf("Updating RootSync to sync from the Helm chart: %s:%s", chart.ChartName, chart.ChartVersion)
+ rs := fake.RootSyncObjectV1Beta1(configsync.RootSyncName)
+ nt.MustMergePatch(rs, fmt.Sprintf(`{
+ "spec": {
+ "sourceType": %q,
+ "helm": {
+ "repo": %q,
+ "chart": %q,
+ "version": %q,
+ "auth": "gcpserviceaccount",
+ "gcpServiceAccountEmail": %q
+ },
+ "git": null
+ }
+ }`,
+ v1beta1.HelmSource,
+ chart.RepositoryOCI(),
+ chart.ChartName,
+ chart.ChartVersion,
+ gsaEmail))
+
+ // Validate that the chart syncs without error
+ err = nt.WatchForAllSyncs(
+ nomostest.WithRootSha1Func(helmChartVersion(chart.ChartVersion)),
+ nomostest.WithSyncDirectoryMap(map[types.NamespacedName]string{
+ nomostest.DefaultRootRepoNamespacedName: chart.ChartName,
+ }))
+ if err != nil {
+ nt.T.Fatal(err)
+ }
+}
+
func helmChartVersion(chartVersion string) nomostest.Sha1Func {
return func(*nomostest.NT, types.NamespacedName) (string, error) {
return chartVersion, nil
diff --git a/e2e/testdata/helm-charts/empty/Chart.yaml b/e2e/testdata/helm-charts/empty/Chart.yaml
new file mode 100644
index 0000000000..be117b90dc
--- /dev/null
+++ b/e2e/testdata/helm-charts/empty/Chart.yaml
@@ -0,0 +1,19 @@
+# Copyright 2023 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+apiVersion: v1
+appVersion: "1.0"
+description: An empty chart with no resources
+name: empty
+version: 1.0.0
diff --git a/go.mod b/go.mod
index 54c7b1f377..3058a7d6dd 100644
--- a/go.mod
+++ b/go.mod
@@ -13,6 +13,7 @@ require (
github.com/Masterminds/semver v1.5.0
github.com/Masterminds/semver/v3 v3.2.1
github.com/davecgh/go-spew v1.1.1
+ github.com/ettle/strcase v0.1.1
github.com/evanphx/json-patch v4.12.0+incompatible
github.com/go-logr/logr v1.2.3
github.com/golang/protobuf v1.5.3
diff --git a/go.sum b/go.sum
index b9d08aa124..a306b96aa2 100644
--- a/go.sum
+++ b/go.sum
@@ -113,6 +113,8 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/ettle/strcase v0.1.1 h1:htFueZyVeE1XNnMEfbqp5r67qAN/4r6ya1ysq8Q+Zcw=
+github.com/ettle/strcase v0.1.1/go.mod h1:hzDLsPC7/lwKyBOywSHEP89nt2pDgdy+No1NBA9o9VY=
github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ=
github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84=
github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
diff --git a/pkg/helm/helm.go b/pkg/helm/helm.go
index bd970c6df1..8eeb0135d1 100644
--- a/pkg/helm/helm.go
+++ b/pkg/helm/helm.go
@@ -182,7 +182,7 @@ func (h *Hydrator) getChartVersion(ctx context.Context) error {
if err := os.RemoveAll(helmCacheHome); err != nil {
// we don't necessarily need to exit on error here, as it is possible that the later rendering
// step could still succeed, so we just log the error and continue
- klog.Infof("failed to clear helm cache: %w\n", err)
+ klog.Infof("failed to clear helm cache: %v", err)
}
klog.Infoln("using chart version: ", h.Version)
return nil
@@ -304,6 +304,11 @@ func (h *Hydrator) HelmTemplate(ctx context.Context) error {
return fmt.Errorf("failed to render the helm chart: %w, stdout: %s", err, string(out))
}
+ // Create the repo/chart directory, in case the chart is empty.
+ if err := os.MkdirAll(filepath.Join(destDir, h.Chart), os.ModePerm); err != nil {
+ return fmt.Errorf("failed to create chart directory: %w", err)
+ }
+
if err := h.setDeployNamespace(destDir); err != nil {
return fmt.Errorf("failed to set the deploy namespace: %w", err)
}
diff --git a/vendor/github.com/ettle/strcase/.gitignore b/vendor/github.com/ettle/strcase/.gitignore
new file mode 100644
index 0000000000..54bc1fbff4
--- /dev/null
+++ b/vendor/github.com/ettle/strcase/.gitignore
@@ -0,0 +1,18 @@
+# Binaries for programs and plugins
+*.exe
+*.exe~
+*.dll
+*.so
+*.dylib
+
+# Test binary, built with `go test -c`
+*.test
+
+# Output of the go coverage tool, specifically when used with LiteIDE
+*.out
+
+# CPU and memory profiles
+*.prof
+
+# Dependency directories
+vendor/
diff --git a/vendor/github.com/ettle/strcase/.golangci.yml b/vendor/github.com/ettle/strcase/.golangci.yml
new file mode 100644
index 0000000000..4d31fcc5b4
--- /dev/null
+++ b/vendor/github.com/ettle/strcase/.golangci.yml
@@ -0,0 +1,88 @@
+linters-settings:
+ dupl:
+ threshold: 100
+ gocyclo:
+ min-complexity: 15
+ gocritic:
+ enabled-tags:
+ - diagnostic
+ - experimental
+ - opinionated
+ - performance
+ - style
+ disabled-checks:
+ - ifElseChain
+ - whyNoLint
+ - wrapperFunc
+ golint:
+ min-confidence: 0.5
+ govet:
+ check-shadowing: true
+ lll:
+ line-length: 140
+ maligned:
+ suggest-new: true
+ misspell:
+ locale: US
+ nolintlint:
+ allow-leading-space: false
+ allow-unused: false
+ require-specific: true
+
+ require-explanation: true
+ allow-no-explanation:
+ - gocyclo
+
+linters:
+ disable-all: true
+ enable:
+ - bodyclose
+ - deadcode
+ - depguard
+ - dogsled
+ - dupl
+ - errcheck
+ - gochecknoinits
+ - gocritic
+ - gocyclo
+ - gofmt
+ - goimports
+ - golint
+ - goprintffuncname
+ - gosec
+ - gosimple
+ - govet
+ - ineffassign
+ - interfacer
+ - lll
+ - misspell
+ - nakedret
+ - nolintlint
+ - rowserrcheck
+ - staticcheck
+ - structcheck
+ - stylecheck
+ - typecheck
+ - unconvert
+ - unparam
+ - unused
+ - varcheck
+ - whitespace
+
+ # don't enable:
+ # - asciicheck
+ # - gochecknoglobals
+ # - gocognit
+ # - godot
+ # - godox
+ # - goerr113
+ # - maligned
+ # - nestif
+ # - prealloc
+ # - testpackage
+ # - wsl
+
+issues:
+ exclude-use-default: false
+ max-issues-per-linter: 0
+ max-same-issues: 0
diff --git a/vendor/github.com/ettle/strcase/.readme.tmpl b/vendor/github.com/ettle/strcase/.readme.tmpl
new file mode 100644
index 0000000000..135765c40a
--- /dev/null
+++ b/vendor/github.com/ettle/strcase/.readme.tmpl
@@ -0,0 +1,80 @@
+{{with .PDoc}}
+# Go Strcase
+
+[![Go Report Card](https://goreportcard.com/badge/github.com/ettle/strcase)](https://goreportcard.com/report/github.com/ettle/strcase)
+[![Coverage](http://gocover.io/_badge/github.com/ettle/strcase?0)](http://gocover.io/github.com/ettle/strcase)
+[![GoDoc](https://godoc.org/github.com/ettle/strcase?status.svg)](https://pkg.go.dev/github.com/ettle/strcase)
+
+Convert strings to `snake_case`, `camelCase`, `PascalCase`, `kebab-case` and more! Supports Go initialisms, customization, and Unicode.
+
+`import "{{.ImportPath}}"`
+
+## Overview
+{{comment_md .Doc}}
+{{example_html $ ""}}
+
+## Index{{if .Consts}}
+* [Constants](#pkg-constants){{end}}{{if .Vars}}
+* [Variables](#pkg-variables){{end}}{{- range .Funcs -}}{{$name_html := html .Name}}
+* [{{node_html $ .Decl false | sanitize}}](#{{$name_html}}){{- end}}{{- range .Types}}{{$tname_html := html .Name}}
+* [type {{$tname_html}}](#{{$tname_html}}){{- range .Funcs}}{{$name_html := html .Name}}
+ * [{{node_html $ .Decl false | sanitize}}](#{{$name_html}}){{- end}}{{- range .Methods}}{{$name_html := html .Name}}
+ * [{{node_html $ .Decl false | sanitize}}](#{{$tname_html}}.{{$name_html}}){{- end}}{{- end}}{{- if $.Notes}}{{- range $marker, $item := $.Notes}}
+* [{{noteTitle $marker | html}}s](#pkg-note-{{$marker}}){{end}}{{end}}
+{{if $.Examples}}
+#### Examples{{- range $.Examples}}
+* [{{example_name .Name}}](#example_{{.Name}}){{- end}}{{- end}}
+
+{{with .Consts}}## Constants
+{{range .}}{{node $ .Decl | pre}}
+{{comment_md .Doc}}{{end}}{{end}}
+{{with .Vars}}## Variables
+{{range .}}{{node $ .Decl | pre}}
+{{comment_md .Doc}}{{end}}{{end}}
+
+{{range .Funcs}}{{$name_html := html .Name}}## func [{{$name_html}}]({{gh_url $ .Decl}})
+{{node $ .Decl | pre}}
+{{comment_md .Doc}}
+{{example_html $ .Name}}
+{{callgraph_html $ "" .Name}}{{end}}
+{{range .Types}}{{$tname := .Name}}{{$tname_html := html .Name}}## type [{{$tname_html}}]({{gh_url $ .Decl}})
+{{node $ .Decl | pre}}
+{{comment_md .Doc}}{{range .Consts}}
+{{node $ .Decl | pre }}
+{{comment_md .Doc}}{{end}}{{range .Vars}}
+{{node $ .Decl | pre }}
+{{comment_md .Doc}}{{end}}
+
+{{example_html $ $tname}}
+{{implements_html $ $tname}}
+{{methodset_html $ $tname}}
+
+{{range .Funcs}}{{$name_html := html .Name}}### func [{{$name_html}}]({{gh_url $ .Decl}})
+{{node $ .Decl | pre}}
+{{comment_md .Doc}}
+{{example_html $ .Name}}{{end}}
+{{callgraph_html $ "" .Name}}
+
+{{range .Methods}}{{$name_html := html .Name}}### func ({{md .Recv}}) [{{$name_html}}]({{gh_url $ .Decl}})
+{{node $ .Decl | pre}}
+{{comment_md .Doc}}
+{{$name := printf "%s_%s" $tname .Name}}{{example_html $ $name}}
+{{callgraph_html $ .Recv .Name}}
+{{end}}{{end}}{{end}}
+
+{{with $.Notes}}
+{{range $marker, $content := .}}
+## {{noteTitle $marker | html}}s
+
+{{range .}}
+- ☞ {{html .Body}}
+{{end}}
+
+{{end}}
+{{end}}
+{{if .Dirs}}
+## Subdirectories
+{{range $.Dirs.List}}
+{{indent .Depth}}* [{{.Name | html}}]({{print "./" .Path}}){{if .Synopsis}} {{ .Synopsis}}{{end -}}
+{{end}}
+{{end}}
diff --git a/vendor/github.com/ettle/strcase/LICENSE b/vendor/github.com/ettle/strcase/LICENSE
new file mode 100644
index 0000000000..4f0116be2e
--- /dev/null
+++ b/vendor/github.com/ettle/strcase/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2020 Liyan David Chang
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/vendor/github.com/ettle/strcase/Makefile b/vendor/github.com/ettle/strcase/Makefile
new file mode 100644
index 0000000000..462f8b473a
--- /dev/null
+++ b/vendor/github.com/ettle/strcase/Makefile
@@ -0,0 +1,16 @@
+.PHONY: benchmark docs lint test
+
+docs:
+ which godoc2ghmd || ( go get github.com/DevotedHealth/godoc2ghmd && go mod tidy )
+ godoc2ghmd -template .readme.tmpl github.com/ettle/strcase > README.md
+
+test:
+ go test -cover ./...
+
+lint:
+ which golangci-lint || ( go get github.com/golangci/golangci-lint/cmd/golangci-lint@v1.27.0 && go mod tidy )
+ golangci-lint run
+ golangci-lint run benchmark/*.go
+
+benchmark:
+ cd benchmark && go test -bench=. -test.benchmem && go mod tidy
diff --git a/vendor/github.com/ettle/strcase/README.md b/vendor/github.com/ettle/strcase/README.md
new file mode 100644
index 0000000000..ee165e3e5e
--- /dev/null
+++ b/vendor/github.com/ettle/strcase/README.md
@@ -0,0 +1,542 @@
+
+# Go Strcase
+
+[![Go Report Card](https://goreportcard.com/badge/github.com/ettle/strcase)](https://goreportcard.com/report/github.com/ettle/strcase)
+[![Coverage](http://gocover.io/_badge/github.com/ettle/strcase?0)](http://gocover.io/github.com/ettle/strcase)
+[![GoDoc](https://godoc.org/github.com/ettle/strcase?status.svg)](https://pkg.go.dev/github.com/ettle/strcase)
+
+Convert strings to `snake_case`, `camelCase`, `PascalCase`, `kebab-case` and more! Supports Go initialisms, customization, and Unicode.
+
+`import "github.com/ettle/strcase"`
+
+## Overview
+Package strcase is a package for converting strings into various word cases
+(e.g. snake_case, camelCase)
+
+
+ go get -u github.com/ettle/strcase
+
+Example usage
+
+
+ strcase.ToSnake("Hello World") // hello_world
+ strcase.ToSNAKE("Hello World") // HELLO_WORLD
+
+ strcase.ToKebab("helloWorld") // hello-world
+ strcase.ToKEBAB("helloWorld") // HELLO-WORLD
+
+ strcase.ToPascal("hello-world") // HelloWorld
+ strcase.ToCamel("hello-world") // helloWorld
+
+ // Handle odd cases
+ strcase.ToSnake("FOOBar") // foo_bar
+
+ // Support Go initialisms
+ strcase.ToGoCamel("http_response") // HTTPResponse
+
+ // Specify case and delimiter
+ strcase.ToCase("HelloWorld", strcase.UpperCase, '.') // HELLO.WORLD
+
+### Why this package
+String strcase is pretty straight forward and there are a number of methods to
+do it. This package is fully featured, more customizable, better tested, and
+faster* than other packages and what you would probably whip up yourself.
+
+### Unicode support
+We work for with unicode strings and pay very little performance penalty for it
+as we optimized for the common use case of ASCII only strings.
+
+### Customization
+You can create a custom caser that changes the behavior to what you want. This
+customization also reduces the pressure for us to change the default behavior
+which means that things are more stable for everyone involved. The goal is to
+make the common path easy and fast, while making the uncommon path possible.
+
+
+ c := NewCaser(
+ // Use Go's default initialisms e.g. ID, HTML
+ true,
+ // Override initialisms (e.g. don't initialize HTML but initialize SSL
+ map[string]bool{"SSL": true, "HTML": false},
+ // Write your own custom SplitFn
+ //
+ NewSplitFn(
+ []rune{'*', '.', ','},
+ SplitCase,
+ SplitAcronym,
+ PreserveNumberFormatting,
+ SplitBeforeNumber,
+ SplitAfterNumber,
+ ))
+ assert.Equal(t, "http_200", c.ToSnake("http200"))
+
+### Initialism support
+By default, we use the golint intialisms list. You can customize and override
+the initialisms if you wish to add additional ones, such as "SSL" or "CMS" or
+domain specific ones to your industry.
+
+
+ ToGoCamel("http_response") // HTTPResponse
+ ToGoSnake("http_response") // HTTP_response
+
+### Test coverage
+We have a wide ranging test suite to make sure that we understand our behavior.
+Test coverage isn't everything, but we aim for 100% coverage.
+
+### Fast
+Optimized to reduce memory allocations with Builder. Benchmarked and optimized
+around common cases.
+
+We're on par with the fastest packages (that have less features) and much
+faster than others. We also benchmarked against code snippets. Using string
+builders to reduce memory allocation and reordering boolean checks for the
+common cases have a large performance impact.
+
+Hopefully I was fair to each library and happy to rerun benchmarks differently
+or reword my commentary based on suggestions or updates.
+
+
+ // This package
+ // Go intialisms and custom casers are slower
+ BenchmarkToTitle-4 992491 1559 ns/op 32 B/op 1 allocs/op
+ BenchmarkToSnake-4 1000000 1475 ns/op 32 B/op 1 allocs/op
+ BenchmarkToSNAKE-4 1000000 1609 ns/op 32 B/op 1 allocs/op
+ BenchmarkToGoSnake-4 275010 3697 ns/op 44 B/op 4 allocs/op
+ BenchmarkToCustomCaser-4 342704 4191 ns/op 56 B/op 4 allocs/op
+
+ // Segment has very fast snake case and camel case libraries
+ // No features or customization, but very very fast
+ BenchmarkSegment-4 1303809 938 ns/op 16 B/op 1 allocs/op
+
+ // Stdlib strings.Title for comparison, even though it only splits on spaces
+ BenchmarkToTitleStrings-4 1213467 1164 ns/op 16 B/op 1 allocs/op
+
+ // Other libraries or code snippets
+ // - Most are slower, by up to an order of magnitude
+ // - None support initialisms or customization
+ // - Some generate only camelCase or snake_case
+ // - Many lack unicode support
+ BenchmarkToSnakeStoewer-4 973200 2075 ns/op 64 B/op 2 allocs/op
+ // Copying small rune arrays is slow
+ BenchmarkToSnakeSiongui-4 264315 4229 ns/op 48 B/op 10 allocs/op
+ BenchmarkGoValidator-4 206811 5152 ns/op 184 B/op 9 allocs/op
+ // String alloction is slow
+ BenchmarkToSnakeFatih-4 82675 12280 ns/op 392 B/op 26 allocs/op
+ BenchmarkToSnakeIanColeman-4 83276 13903 ns/op 145 B/op 13 allocs/op
+ // Regexp is slow
+ BenchmarkToSnakeGolangPrograms-4 74448 18586 ns/op 176 B/op 11 allocs/op
+
+ // These results aren't a surprise - my initial version of this library was
+ // painfully slow. I think most of us, without spending some time with
+ // profilers and benchmarks, would write also something on the slower side.
+
+### Why not this package
+If every nanosecond matters and this is used in a tight loop, use segment.io's
+libraries (https://github.com/segmentio/go-snakecase and
+https://github.com/segmentio/go-camelcase). They lack features, but make up for
+it by being blazing fast. Alternatively, if you need your code to work slightly
+differently, fork them and tailor it for your use case.
+
+If you don't like having external imports, I get it. This package only imports
+packages for testing, otherwise it only uses the standard library. If that's
+not enough, you can use this repo as the foundation for your own. MIT Licensed.
+
+This package is still relatively new and while I've used it for a while
+personally, it doesn't have the miles that other packages do. I've tested this
+code agains't their test cases to make sure that there aren't any surprises.
+
+### Migrating from other packages
+If you are migrating from from another package, you may find slight differences
+in output. To reduce the delta, you may find it helpful to use the following
+custom casers to mimic the behavior of the other package.
+
+
+ // From https://github.com/iancoleman/strcase
+ var c = NewCaser(false, nil, NewSplitFn([]rune{'_', '-', '.'}, SplitCase, SplitAcronym, SplitBeforeNumber))
+
+ // From https://github.com/stoewer/go-strcase
+ var c = NewCaser(false, nil, NewSplitFn([]rune{'_', '-'}, SplitCase), SplitAcronym)
+
+
+
+
+## Index
+* [func ToCamel(s string) string](#ToCamel)
+* [func ToCase(s string, wordCase WordCase, delimiter rune) string](#ToCase)
+* [func ToGoCamel(s string) string](#ToGoCamel)
+* [func ToGoCase(s string, wordCase WordCase, delimiter rune) string](#ToGoCase)
+* [func ToGoKebab(s string) string](#ToGoKebab)
+* [func ToGoPascal(s string) string](#ToGoPascal)
+* [func ToGoSnake(s string) string](#ToGoSnake)
+* [func ToKEBAB(s string) string](#ToKEBAB)
+* [func ToKebab(s string) string](#ToKebab)
+* [func ToPascal(s string) string](#ToPascal)
+* [func ToSNAKE(s string) string](#ToSNAKE)
+* [func ToSnake(s string) string](#ToSnake)
+* [type Caser](#Caser)
+ * [func NewCaser(goInitialisms bool, initialismOverrides map[string]bool, splitFn SplitFn) *Caser](#NewCaser)
+ * [func (c *Caser) ToCamel(s string) string](#Caser.ToCamel)
+ * [func (c *Caser) ToCase(s string, wordCase WordCase, delimiter rune) string](#Caser.ToCase)
+ * [func (c *Caser) ToKEBAB(s string) string](#Caser.ToKEBAB)
+ * [func (c *Caser) ToKebab(s string) string](#Caser.ToKebab)
+ * [func (c *Caser) ToPascal(s string) string](#Caser.ToPascal)
+ * [func (c *Caser) ToSNAKE(s string) string](#Caser.ToSNAKE)
+ * [func (c *Caser) ToSnake(s string) string](#Caser.ToSnake)
+* [type SplitAction](#SplitAction)
+* [type SplitFn](#SplitFn)
+ * [func NewSplitFn(delimiters []rune, splitOptions ...SplitOption) SplitFn](#NewSplitFn)
+* [type SplitOption](#SplitOption)
+* [type WordCase](#WordCase)
+
+
+
+
+
+## func [ToCamel](./strcase.go#L57)
+``` go
+func ToCamel(s string) string
+```
+ToCamel returns words in camelCase (capitalized words concatenated together, with first word lower case).
+Also known as lowerCamelCase or mixedCase.
+
+
+
+## func [ToCase](./strcase.go#L70)
+``` go
+func ToCase(s string, wordCase WordCase, delimiter rune) string
+```
+ToCase returns words in given case and delimiter.
+
+
+
+## func [ToGoCamel](./strcase.go#L65)
+``` go
+func ToGoCamel(s string) string
+```
+ToGoCamel returns words in camelCase (capitalized words concatenated together, with first word lower case).
+Also known as lowerCamelCase or mixedCase.
+
+Respects Go's common initialisms (e.g. httpResponse -> HTTPResponse).
+
+
+
+## func [ToGoCase](./strcase.go#L77)
+``` go
+func ToGoCase(s string, wordCase WordCase, delimiter rune) string
+```
+ToGoCase returns words in given case and delimiter.
+
+Respects Go's common initialisms (e.g. httpResponse -> HTTPResponse).
+
+
+
+## func [ToGoKebab](./strcase.go#L31)
+``` go
+func ToGoKebab(s string) string
+```
+ToGoKebab returns words in kebab-case (lower case words with dashes).
+Also known as dash-case.
+
+Respects Go's common initialisms (e.g. http-response -> HTTP-response).
+
+
+
+## func [ToGoPascal](./strcase.go#L51)
+``` go
+func ToGoPascal(s string) string
+```
+ToGoPascal returns words in PascalCase (capitalized words concatenated together).
+Also known as UpperPascalCase.
+
+Respects Go's common initialisms (e.g. HttpResponse -> HTTPResponse).
+
+
+
+## func [ToGoSnake](./strcase.go#L11)
+``` go
+func ToGoSnake(s string) string
+```
+ToGoSnake returns words in snake_case (lower case words with underscores).
+
+Respects Go's common initialisms (e.g. http_response -> HTTP_response).
+
+
+
+## func [ToKEBAB](./strcase.go#L37)
+``` go
+func ToKEBAB(s string) string
+```
+ToKEBAB returns words in KEBAB-CASE (upper case words with dashes).
+Also known as SCREAMING-KEBAB-CASE or SCREAMING-DASH-CASE.
+
+
+
+## func [ToKebab](./strcase.go#L23)
+``` go
+func ToKebab(s string) string
+```
+ToKebab returns words in kebab-case (lower case words with dashes).
+Also known as dash-case.
+
+
+
+## func [ToPascal](./strcase.go#L43)
+``` go
+func ToPascal(s string) string
+```
+ToPascal returns words in PascalCase (capitalized words concatenated together).
+Also known as UpperPascalCase.
+
+
+
+## func [ToSNAKE](./strcase.go#L17)
+``` go
+func ToSNAKE(s string) string
+```
+ToSNAKE returns words in SNAKE_CASE (upper case words with underscores).
+Also known as SCREAMING_SNAKE_CASE or UPPER_CASE.
+
+
+
+## func [ToSnake](./strcase.go#L4)
+``` go
+func ToSnake(s string) string
+```
+ToSnake returns words in snake_case (lower case words with underscores).
+
+
+
+
+## type [Caser](./caser.go#L4-L7)
+``` go
+type Caser struct {
+ // contains filtered or unexported fields
+}
+
+```
+Caser allows for customization of parsing and intialisms
+
+
+
+
+
+
+
+### func [NewCaser](./caser.go#L24)
+``` go
+func NewCaser(goInitialisms bool, initialismOverrides map[string]bool, splitFn SplitFn) *Caser
+```
+NewCaser returns a configured Caser.
+
+A Caser should be created when you want fine grained control over how the words are split.
+
+
+ Notes on function arguments
+
+ goInitialisms: Whether to use Golint's intialisms
+
+ initialismOverrides: A mapping of extra initialisms
+ Keys must be in ALL CAPS. Merged with Golint's if goInitialisms is set.
+ Setting a key to false will override Golint's.
+
+ splitFn: How to separate words
+ Override the default split function. Consider using NewSplitFn to
+ configure one instead of writing your own.
+
+
+
+
+
+### func (\*Caser) [ToCamel](./caser.go#L80)
+``` go
+func (c *Caser) ToCamel(s string) string
+```
+ToCamel returns words in camelCase (capitalized words concatenated together, with first word lower case).
+Also known as lowerCamelCase or mixedCase.
+
+
+
+
+### func (\*Caser) [ToCase](./caser.go#L85)
+``` go
+func (c *Caser) ToCase(s string, wordCase WordCase, delimiter rune) string
+```
+ToCase returns words with a given case and delimiter.
+
+
+
+
+### func (\*Caser) [ToKEBAB](./caser.go#L68)
+``` go
+func (c *Caser) ToKEBAB(s string) string
+```
+ToKEBAB returns words in KEBAB-CASE (upper case words with dashes).
+Also known as SCREAMING-KEBAB-CASE or SCREAMING-DASH-CASE.
+
+
+
+
+### func (\*Caser) [ToKebab](./caser.go#L62)
+``` go
+func (c *Caser) ToKebab(s string) string
+```
+ToKebab returns words in kebab-case (lower case words with dashes).
+Also known as dash-case.
+
+
+
+
+### func (\*Caser) [ToPascal](./caser.go#L74)
+``` go
+func (c *Caser) ToPascal(s string) string
+```
+ToPascal returns words in PascalCase (capitalized words concatenated together).
+Also known as UpperPascalCase.
+
+
+
+
+### func (\*Caser) [ToSNAKE](./caser.go#L56)
+``` go
+func (c *Caser) ToSNAKE(s string) string
+```
+ToSNAKE returns words in SNAKE_CASE (upper case words with underscores).
+Also known as SCREAMING_SNAKE_CASE or UPPER_CASE.
+
+
+
+
+### func (\*Caser) [ToSnake](./caser.go#L50)
+``` go
+func (c *Caser) ToSnake(s string) string
+```
+ToSnake returns words in snake_case (lower case words with underscores).
+
+
+
+
+## type [SplitAction](./split.go#L110)
+``` go
+type SplitAction int
+```
+SplitAction defines if and how to split a string
+
+
+``` go
+const (
+ // Noop - Continue to next character
+ Noop SplitAction = iota
+ // Split - Split between words
+ // e.g. to split between wordsWithoutDelimiters
+ Split
+ // SkipSplit - Split the word and drop the character
+ // e.g. to split words with delimiters
+ SkipSplit
+ // Skip - Remove the character completely
+ Skip
+)
+```
+
+
+
+
+
+
+
+
+
+## type [SplitFn](./split.go#L6)
+``` go
+type SplitFn func(prev, curr, next rune) SplitAction
+```
+SplitFn defines how to split a string into words
+
+
+
+
+
+
+
+### func [NewSplitFn](./split.go#L14-L17)
+``` go
+func NewSplitFn(
+ delimiters []rune,
+ splitOptions ...SplitOption,
+) SplitFn
+```
+NewSplitFn returns a SplitFn based on the options provided.
+
+NewSplitFn covers the majority of common options that other strcase
+libraries provide and should allow you to simply create a custom caser.
+For more complicated use cases, feel free to write your own SplitFn
+nolint:gocyclo
+
+
+
+
+
+## type [SplitOption](./split.go#L93)
+``` go
+type SplitOption int
+```
+SplitOption are options that allow for configuring NewSplitFn
+
+
+``` go
+const (
+ // SplitCase - FooBar -> Foo_Bar
+ SplitCase SplitOption = iota
+ // SplitAcronym - FOOBar -> Foo_Bar
+ // It won't preserve FOO's case. If you want, you can set the Caser's initialisms so FOO will be in all caps
+ SplitAcronym
+ // SplitBeforeNumber - port80 -> port_80
+ SplitBeforeNumber
+ // SplitAfterNumber - 200status -> 200_status
+ SplitAfterNumber
+ // PreserveNumberFormatting - a.b.2,000.3.c -> a_b_2,000.3_c
+ PreserveNumberFormatting
+)
+```
+
+
+
+
+
+
+
+
+
+## type [WordCase](./convert.go#L6)
+``` go
+type WordCase int
+```
+WordCase is an enumeration of the ways to format a word.
+
+
+``` go
+const (
+ // Original - Preserve the original input strcase
+ Original WordCase = iota
+ // LowerCase - All letters lower cased (example)
+ LowerCase
+ // UpperCase - All letters upper cased (EXAMPLE)
+ UpperCase
+ // TitleCase - Only first letter upper cased (Example)
+ TitleCase
+ // CamelCase - TitleCase except lower case first word (exampleText)
+ CamelCase
+)
+```
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vendor/github.com/ettle/strcase/caser.go b/vendor/github.com/ettle/strcase/caser.go
new file mode 100644
index 0000000000..891a671897
--- /dev/null
+++ b/vendor/github.com/ettle/strcase/caser.go
@@ -0,0 +1,87 @@
+package strcase
+
+// Caser allows for customization of parsing and intialisms
+type Caser struct {
+ initialisms map[string]bool
+ splitFn SplitFn
+}
+
+// NewCaser returns a configured Caser.
+//
+// A Caser should be created when you want fine grained control over how the words are split.
+//
+// Notes on function arguments
+//
+// goInitialisms: Whether to use Golint's intialisms
+//
+// initialismOverrides: A mapping of extra initialisms
+// Keys must be in ALL CAPS. Merged with Golint's if goInitialisms is set.
+// Setting a key to false will override Golint's.
+//
+// splitFn: How to separate words
+// Override the default split function. Consider using NewSplitFn to
+// configure one instead of writing your own.
+func NewCaser(goInitialisms bool, initialismOverrides map[string]bool, splitFn SplitFn) *Caser {
+ c := &Caser{
+ initialisms: golintInitialisms,
+ splitFn: splitFn,
+ }
+
+ if c.splitFn == nil {
+ c.splitFn = defaultSplitFn
+ }
+
+ if goInitialisms && initialismOverrides != nil {
+ c.initialisms = map[string]bool{}
+ for k, v := range golintInitialisms {
+ c.initialisms[k] = v
+ }
+ for k, v := range initialismOverrides {
+ c.initialisms[k] = v
+ }
+ } else if !goInitialisms {
+ c.initialisms = initialismOverrides
+ }
+
+ return c
+}
+
+// ToSnake returns words in snake_case (lower case words with underscores).
+func (c *Caser) ToSnake(s string) string {
+ return convert(s, c.splitFn, '_', LowerCase, c.initialisms)
+}
+
+// ToSNAKE returns words in SNAKE_CASE (upper case words with underscores).
+// Also known as SCREAMING_SNAKE_CASE or UPPER_CASE.
+func (c *Caser) ToSNAKE(s string) string {
+ return convert(s, c.splitFn, '_', UpperCase, c.initialisms)
+}
+
+// ToKebab returns words in kebab-case (lower case words with dashes).
+// Also known as dash-case.
+func (c *Caser) ToKebab(s string) string {
+ return convert(s, c.splitFn, '-', LowerCase, c.initialisms)
+}
+
+// ToKEBAB returns words in KEBAB-CASE (upper case words with dashes).
+// Also known as SCREAMING-KEBAB-CASE or SCREAMING-DASH-CASE.
+func (c *Caser) ToKEBAB(s string) string {
+ return convert(s, c.splitFn, '-', UpperCase, c.initialisms)
+}
+
+// ToPascal returns words in PascalCase (capitalized words concatenated together).
+// Also known as UpperPascalCase.
+func (c *Caser) ToPascal(s string) string {
+ return convert(s, c.splitFn, '\x00', TitleCase, c.initialisms)
+}
+
+// ToCamel returns words in camelCase (capitalized words concatenated together, with first word lower case).
+// Also known as lowerCamelCase or mixedCase.
+func (c *Caser) ToCamel(s string) string {
+ return convert(s, c.splitFn, '\x00', CamelCase, c.initialisms)
+}
+
+// ToCase returns words with a given case and delimiter.
+func (c *Caser) ToCase(s string, wordCase WordCase, delimiter rune) string {
+ return convert(s, c.splitFn, delimiter, wordCase, c.initialisms)
+}
diff --git a/vendor/github.com/ettle/strcase/convert.go b/vendor/github.com/ettle/strcase/convert.go
new file mode 100644
index 0000000000..70fedb1449
--- /dev/null
+++ b/vendor/github.com/ettle/strcase/convert.go
@@ -0,0 +1,297 @@
+package strcase
+
+import "strings"
+
+// WordCase is an enumeration of the ways to format a word.
+type WordCase int
+
+const (
+ // Original - Preserve the original input strcase
+ Original WordCase = iota
+ // LowerCase - All letters lower cased (example)
+ LowerCase
+ // UpperCase - All letters upper cased (EXAMPLE)
+ UpperCase
+ // TitleCase - Only first letter upper cased (Example)
+ TitleCase
+ // CamelCase - TitleCase except lower case first word (exampleText)
+ // Notably, even if the first word is an initialism, it will be lower
+ // cased. This is important for code generators where capital letters
+ // mean exported functions. i.e. jsonString(), not JSONString()
+ CamelCase
+)
+
+// We have 3 convert functions for performance reasons
+// The general convert could handle everything, but is not optimized
+//
+// The other two functions are optimized for the general use cases - that is the non-custom caser functions
+// Case 1: Any Case and supports Go Initialisms
+// Case 2: UpperCase words, which don't need to support initialisms since everything is in upper case
+
+// convertWithoutInitialims only works for to UpperCase and LowerCase
+//nolint:gocyclo
+func convertWithoutInitialisms(input string, delimiter rune, wordCase WordCase) string {
+ input = strings.TrimSpace(input)
+ runes := []rune(input)
+ if len(runes) == 0 {
+ return ""
+ }
+
+ var b strings.Builder
+ b.Grow(len(input) * 2) // In case we need to write delimiters where they weren't before
+
+ var prev, curr rune
+ next := runes[0] // 0 length will have already returned so safe to index
+ inWord := false
+ firstWord := true
+ for i := 0; i < len(runes); i++ {
+ prev = curr
+ curr = next
+ if i+1 == len(runes) {
+ next = 0
+ } else {
+ next = runes[i+1]
+ }
+
+ switch defaultSplitFn(prev, curr, next) {
+ case SkipSplit:
+ if inWord && delimiter != 0 {
+ b.WriteRune(delimiter)
+ }
+ inWord = false
+ continue
+ case Split:
+ if inWord && delimiter != 0 {
+ b.WriteRune(delimiter)
+ }
+ inWord = false
+ }
+ switch wordCase {
+ case UpperCase:
+ b.WriteRune(toUpper(curr))
+ case LowerCase:
+ b.WriteRune(toLower(curr))
+ case TitleCase:
+ if inWord {
+ b.WriteRune(toLower(curr))
+ } else {
+ b.WriteRune(toUpper(curr))
+ }
+ case CamelCase:
+ if inWord {
+ b.WriteRune(toLower(curr))
+ } else if firstWord {
+ b.WriteRune(toLower(curr))
+ firstWord = false
+ } else {
+ b.WriteRune(toUpper(curr))
+ }
+ default:
+ // Must be original case
+ b.WriteRune(curr)
+ }
+ inWord = inWord || true
+ }
+ return b.String()
+}
+
+// convertWithGoInitialisms changes a input string to a certain case with a
+// delimiter, respecting go initialisms but not skip runes
+//nolint:gocyclo
+func convertWithGoInitialisms(input string, delimiter rune, wordCase WordCase) string {
+ input = strings.TrimSpace(input)
+ runes := []rune(input)
+ if len(runes) == 0 {
+ return ""
+ }
+
+ var b strings.Builder
+ b.Grow(len(input) * 2) // In case we need to write delimiters where they weren't before
+
+ firstWord := true
+
+ addWord := func(start, end int) {
+ if start == end {
+ return
+ }
+
+ if !firstWord && delimiter != 0 {
+ b.WriteRune(delimiter)
+ }
+
+ // Don't bother with initialisms if the word is longer than 5
+ // A quick proxy to avoid the extra memory allocations
+ if end-start <= 5 {
+ key := strings.ToUpper(string(runes[start:end]))
+ if golintInitialisms[key] {
+ if !firstWord || wordCase != CamelCase {
+ b.WriteString(key)
+ firstWord = false
+ return
+ }
+ }
+ }
+
+ for i := start; i < end; i++ {
+ r := runes[i]
+ switch wordCase {
+ case UpperCase:
+ panic("use convertWithoutInitialisms instead")
+ case LowerCase:
+ b.WriteRune(toLower(r))
+ case TitleCase:
+ if i == start {
+ b.WriteRune(toUpper(r))
+ } else {
+ b.WriteRune(toLower(r))
+ }
+ case CamelCase:
+ if !firstWord && i == start {
+ b.WriteRune(toUpper(r))
+ } else {
+ b.WriteRune(toLower(r))
+ }
+ default:
+ b.WriteRune(r)
+ }
+ }
+ firstWord = false
+ }
+
+ var prev, curr rune
+ next := runes[0] // 0 length will have already returned so safe to index
+ wordStart := 0
+ for i := 0; i < len(runes); i++ {
+ prev = curr
+ curr = next
+ if i+1 == len(runes) {
+ next = 0
+ } else {
+ next = runes[i+1]
+ }
+
+ switch defaultSplitFn(prev, curr, next) {
+ case Split:
+ addWord(wordStart, i)
+ wordStart = i
+ case SkipSplit:
+ addWord(wordStart, i)
+ wordStart = i + 1
+ }
+ }
+
+ if wordStart != len(runes) {
+ addWord(wordStart, len(runes))
+ }
+ return b.String()
+}
+
+// convert changes a input string to a certain case with a delimiter,
+// respecting arbitrary initialisms and skip characters
+//nolint:gocyclo
+func convert(input string, fn SplitFn, delimiter rune, wordCase WordCase,
+ initialisms map[string]bool) string {
+ input = strings.TrimSpace(input)
+ runes := []rune(input)
+ if len(runes) == 0 {
+ return ""
+ }
+
+ var b strings.Builder
+ b.Grow(len(input) * 2) // In case we need to write delimiters where they weren't before
+
+ firstWord := true
+ var skipIndexes []int
+
+ addWord := func(start, end int) {
+ // If you have nothing good to say, say nothing at all
+ if start == end || len(skipIndexes) == end-start {
+ skipIndexes = nil
+ return
+ }
+
+ // If you have something to say, start with a delimiter
+ if !firstWord && delimiter != 0 {
+ b.WriteRune(delimiter)
+ }
+
+ // Check if you're an initialism
+ // Note - we don't check skip characters here since initialisms
+ // will probably never have junk characters in between
+ // I'm open to it if there is a use case
+ if initialisms != nil {
+ var word strings.Builder
+ for i := start; i < end; i++ {
+ word.WriteRune(toUpper(runes[i]))
+ }
+ key := word.String()
+ if initialisms[key] {
+ if !firstWord || wordCase != CamelCase {
+ b.WriteString(key)
+ firstWord = false
+ return
+ }
+ }
+ }
+
+ skipIdx := 0
+ for i := start; i < end; i++ {
+ if len(skipIndexes) > 0 && skipIdx < len(skipIndexes) && i == skipIndexes[skipIdx] {
+ skipIdx++
+ continue
+ }
+ r := runes[i]
+ switch wordCase {
+ case UpperCase:
+ b.WriteRune(toUpper(r))
+ case LowerCase:
+ b.WriteRune(toLower(r))
+ case TitleCase:
+ if i == start {
+ b.WriteRune(toUpper(r))
+ } else {
+ b.WriteRune(toLower(r))
+ }
+ case CamelCase:
+ if !firstWord && i == start {
+ b.WriteRune(toUpper(r))
+ } else {
+ b.WriteRune(toLower(r))
+ }
+ default:
+ b.WriteRune(r)
+ }
+ }
+ firstWord = false
+ skipIndexes = nil
+ }
+
+ var prev, curr rune
+ next := runes[0] // 0 length will have already returned so safe to index
+ wordStart := 0
+ for i := 0; i < len(runes); i++ {
+ prev = curr
+ curr = next
+ if i+1 == len(runes) {
+ next = 0
+ } else {
+ next = runes[i+1]
+ }
+
+ switch fn(prev, curr, next) {
+ case Skip:
+ skipIndexes = append(skipIndexes, i)
+ case Split:
+ addWord(wordStart, i)
+ wordStart = i
+ case SkipSplit:
+ addWord(wordStart, i)
+ wordStart = i + 1
+ }
+ }
+
+ if wordStart != len(runes) {
+ addWord(wordStart, len(runes))
+ }
+ return b.String()
+}
diff --git a/vendor/github.com/ettle/strcase/doc.go b/vendor/github.com/ettle/strcase/doc.go
new file mode 100644
index 0000000000..b898a4e45f
--- /dev/null
+++ b/vendor/github.com/ettle/strcase/doc.go
@@ -0,0 +1,155 @@
+/*
+Package strcase is a package for converting strings into various word cases
+(e.g. snake_case, camelCase)
+
+ go get -u github.com/ettle/strcase
+
+Example usage
+
+ strcase.ToSnake("Hello World") // hello_world
+ strcase.ToSNAKE("Hello World") // HELLO_WORLD
+
+ strcase.ToKebab("helloWorld") // hello-world
+ strcase.ToKEBAB("helloWorld") // HELLO-WORLD
+
+ strcase.ToPascal("hello-world") // HelloWorld
+ strcase.ToCamel("hello-world") // helloWorld
+
+ // Handle odd cases
+ strcase.ToSnake("FOOBar") // foo_bar
+
+ // Support Go initialisms
+ strcase.ToGoPascal("http_response") // HTTPResponse
+
+ // Specify case and delimiter
+ strcase.ToCase("HelloWorld", strcase.UpperCase, '.') // HELLO.WORLD
+
+Why this package
+
+String strcase is pretty straight forward and there are a number of methods to
+do it. This package is fully featured, more customizable, better tested, and
+faster* than other packages and what you would probably whip up yourself.
+
+Unicode support
+
+We work for with unicode strings and pay very little performance penalty for it
+as we optimized for the common use case of ASCII only strings.
+
+Customization
+
+You can create a custom caser that changes the behavior to what you want. This
+customization also reduces the pressure for us to change the default behavior
+which means that things are more stable for everyone involved. The goal is to
+make the common path easy and fast, while making the uncommon path possible.
+
+ c := NewCaser(
+ // Use Go's default initialisms e.g. ID, HTML
+ true,
+ // Override initialisms (e.g. don't initialize HTML but initialize SSL
+ map[string]bool{"SSL": true, "HTML": false},
+ // Write your own custom SplitFn
+ //
+ NewSplitFn(
+ []rune{'*', '.', ','},
+ SplitCase,
+ SplitAcronym,
+ PreserveNumberFormatting,
+ SplitBeforeNumber,
+ SplitAfterNumber,
+ ))
+ assert.Equal(t, "http_200", c.ToSnake("http200"))
+
+Initialism support
+
+By default, we use the golint intialisms list. You can customize and override
+the initialisms if you wish to add additional ones, such as "SSL" or "CMS" or
+domain specific ones to your industry.
+
+ ToGoPascal("http_response") // HTTPResponse
+ ToGoSnake("http_response") // HTTP_response
+
+Test coverage
+
+We have a wide ranging test suite to make sure that we understand our behavior.
+Test coverage isn't everything, but we aim for 100% coverage.
+
+Fast
+
+Optimized to reduce memory allocations with Builder. Benchmarked and optimized
+around common cases.
+
+We're on par with the fastest packages (that have less features) and much
+faster than others. We also benchmarked against code snippets. Using string
+builders to reduce memory allocation and reordering boolean checks for the
+common cases have a large performance impact.
+
+Hopefully I was fair to each library and happy to rerun benchmarks differently
+or reword my commentary based on suggestions or updates.
+
+ // This package - faster then almost all libraries
+ // Initialisms are more complicated and slightly slower, but still faster then other libraries that do less
+ BenchmarkToTitle-4 7821166 221 ns/op 32 B/op 1 allocs/op
+ BenchmarkToSnake-4 9378589 202 ns/op 32 B/op 1 allocs/op
+ BenchmarkToSNAKE-4 6174453 223 ns/op 32 B/op 1 allocs/op
+ BenchmarkToGoSnake-4 3114266 434 ns/op 44 B/op 4 allocs/op
+ BenchmarkToCustomCaser-4 2973855 448 ns/op 56 B/op 4 allocs/op
+
+ // Segment has very fast snake case and camel case libraries
+ // No features or customization, but very very fast
+ BenchmarkSegment-4 24003495 64.9 ns/op 16 B/op 1 allocs/op
+
+ // Stdlib strings.Title for comparison, even though it only splits on spaces
+ BenchmarkToTitleStrings-4 11259376 161 ns/op 16 B/op 1 allocs/op
+
+ // Other libraries or code snippets
+ // - Most are slower, by up to an order of magnitude
+ // - None support initialisms or customization
+ // - Some generate only camelCase or snake_case
+ // - Many lack unicode support
+ BenchmarkToSnakeStoewer-4 7103268 297 ns/op 64 B/op 2 allocs/op
+ // Copying small rune arrays is slow
+ BenchmarkToSnakeSiongui-4 3710768 413 ns/op 48 B/op 10 allocs/op
+ BenchmarkGoValidator-4 2416479 1049 ns/op 184 B/op 9 allocs/op
+ // String alloction is slow
+ BenchmarkToSnakeFatih-4 1000000 2407 ns/op 624 B/op 26 allocs/op
+ BenchmarkToSnakeIanColeman-4 1005766 1426 ns/op 160 B/op 13 allocs/op
+ // Regexp is slow
+ BenchmarkToSnakeGolangPrograms-4 614689 2237 ns/op 225 B/op 11 allocs/op
+
+
+
+ // These results aren't a surprise - my initial version of this library was
+ // painfully slow. I think most of us, without spending some time with
+ // profilers and benchmarks, would write also something on the slower side.
+
+
+Why not this package
+
+If every nanosecond matters and this is used in a tight loop, use segment.io's
+libraries (https://github.com/segmentio/go-snakecase and
+https://github.com/segmentio/go-camelcase). They lack features, but make up for
+it by being blazing fast. Alternatively, if you need your code to work slightly
+differently, fork them and tailor it for your use case.
+
+If you don't like having external imports, I get it. This package only imports
+packages for testing, otherwise it only uses the standard library. If that's
+not enough, you can use this repo as the foundation for your own. MIT Licensed.
+
+This package is still relatively new and while I've used it for a while
+personally, it doesn't have the miles that other packages do. I've tested this
+code agains't their test cases to make sure that there aren't any surprises.
+
+Migrating from other packages
+
+If you are migrating from from another package, you may find slight differences
+in output. To reduce the delta, you may find it helpful to use the following
+custom casers to mimic the behavior of the other package.
+
+ // From https://github.com/iancoleman/strcase
+ var c = NewCaser(false, nil, NewSplitFn([]rune{'_', '-', '.'}, SplitCase, SplitAcronym, SplitBeforeNumber))
+
+ // From https://github.com/stoewer/go-strcase
+ var c = NewCaser(false, nil, NewSplitFn([]rune{'_', '-'}, SplitCase), SplitAcronym)
+
+*/
+package strcase
diff --git a/vendor/github.com/ettle/strcase/initialism.go b/vendor/github.com/ettle/strcase/initialism.go
new file mode 100644
index 0000000000..3c313d3e9a
--- /dev/null
+++ b/vendor/github.com/ettle/strcase/initialism.go
@@ -0,0 +1,43 @@
+package strcase
+
+// golintInitialisms are the golint initialisms
+var golintInitialisms = map[string]bool{
+ "ACL": true,
+ "API": true,
+ "ASCII": true,
+ "CPU": true,
+ "CSS": true,
+ "DNS": true,
+ "EOF": true,
+ "GUID": true,
+ "HTML": true,
+ "HTTP": true,
+ "HTTPS": true,
+ "ID": true,
+ "IP": true,
+ "JSON": true,
+ "LHS": true,
+ "QPS": true,
+ "RAM": true,
+ "RHS": true,
+ "RPC": true,
+ "SLA": true,
+ "SMTP": true,
+ "SQL": true,
+ "SSH": true,
+ "TCP": true,
+ "TLS": true,
+ "TTL": true,
+ "UDP": true,
+ "UI": true,
+ "UID": true,
+ "UUID": true,
+ "URI": true,
+ "URL": true,
+ "UTF8": true,
+ "VM": true,
+ "XML": true,
+ "XMPP": true,
+ "XSRF": true,
+ "XSS": true,
+}
diff --git a/vendor/github.com/ettle/strcase/split.go b/vendor/github.com/ettle/strcase/split.go
new file mode 100644
index 0000000000..84381106bc
--- /dev/null
+++ b/vendor/github.com/ettle/strcase/split.go
@@ -0,0 +1,164 @@
+package strcase
+
+import "unicode"
+
+// SplitFn defines how to split a string into words
+type SplitFn func(prev, curr, next rune) SplitAction
+
+// NewSplitFn returns a SplitFn based on the options provided.
+//
+// NewSplitFn covers the majority of common options that other strcase
+// libraries provide and should allow you to simply create a custom caser.
+// For more complicated use cases, feel free to write your own SplitFn
+//nolint:gocyclo
+func NewSplitFn(
+ delimiters []rune,
+ splitOptions ...SplitOption,
+) SplitFn {
+ var splitCase, splitAcronym, splitBeforeNumber, splitAfterNumber, preserveNumberFormatting bool
+
+ for _, option := range splitOptions {
+ switch option {
+ case SplitCase:
+ splitCase = true
+ case SplitAcronym:
+ splitAcronym = true
+ case SplitBeforeNumber:
+ splitBeforeNumber = true
+ case SplitAfterNumber:
+ splitAfterNumber = true
+ case PreserveNumberFormatting:
+ preserveNumberFormatting = true
+ }
+ }
+
+ return func(prev, curr, next rune) SplitAction {
+ // The most common case will be that it's just a letter
+ // There are safe cases to process
+ if isLower(curr) && !isNumber(prev) {
+ return Noop
+ }
+ if isUpper(prev) && isUpper(curr) && isUpper(next) {
+ return Noop
+ }
+
+ if preserveNumberFormatting {
+ if (curr == '.' || curr == ',') &&
+ isNumber(prev) && isNumber(next) {
+ return Noop
+ }
+ }
+
+ if unicode.IsSpace(curr) {
+ return SkipSplit
+ }
+ for _, d := range delimiters {
+ if curr == d {
+ return SkipSplit
+ }
+ }
+
+ if splitBeforeNumber {
+ if isNumber(curr) && !isNumber(prev) {
+ if preserveNumberFormatting && (prev == '.' || prev == ',') {
+ return Noop
+ }
+ return Split
+ }
+ }
+
+ if splitAfterNumber {
+ if isNumber(prev) && !isNumber(curr) {
+ return Split
+ }
+ }
+
+ if splitCase {
+ if !isUpper(prev) && isUpper(curr) {
+ return Split
+ }
+ }
+
+ if splitAcronym {
+ if isUpper(prev) && isUpper(curr) && isLower(next) {
+ return Split
+ }
+ }
+
+ return Noop
+ }
+}
+
+// SplitOption are options that allow for configuring NewSplitFn
+type SplitOption int
+
+const (
+ // SplitCase - FooBar -> Foo_Bar
+ SplitCase SplitOption = iota
+ // SplitAcronym - FOOBar -> Foo_Bar
+ // It won't preserve FOO's case. If you want, you can set the Caser's initialisms so FOO will be in all caps
+ SplitAcronym
+ // SplitBeforeNumber - port80 -> port_80
+ SplitBeforeNumber
+ // SplitAfterNumber - 200status -> 200_status
+ SplitAfterNumber
+ // PreserveNumberFormatting - a.b.2,000.3.c -> a_b_2,000.3_c
+ PreserveNumberFormatting
+)
+
+// SplitAction defines if and how to split a string
+type SplitAction int
+
+const (
+ // Noop - Continue to next character
+ Noop SplitAction = iota
+ // Split - Split between words
+ // e.g. to split between wordsWithoutDelimiters
+ Split
+ // SkipSplit - Split the word and drop the character
+ // e.g. to split words with delimiters
+ SkipSplit
+ // Skip - Remove the character completely
+ Skip
+)
+
+//nolint:gocyclo
+func defaultSplitFn(prev, curr, next rune) SplitAction {
+ // The most common case will be that it's just a letter so let lowercase letters return early since we know what they should do
+ if isLower(curr) {
+ return Noop
+ }
+ // Delimiters are _, -, ., and unicode spaces
+ // Handle . lower down as it needs to happen after number exceptions
+ if curr == '_' || curr == '-' || isSpace(curr) {
+ return SkipSplit
+ }
+
+ if isUpper(curr) {
+ if isLower(prev) {
+ // fooBar
+ return Split
+ } else if isUpper(prev) && isLower(next) {
+ // FOOBar
+ return Split
+ }
+ }
+
+ // Do numeric exceptions last to avoid perf penalty
+ if unicode.IsNumber(prev) {
+ // v4.3 is not split
+ if (curr == '.' || curr == ',') && unicode.IsNumber(next) {
+ return Noop
+ }
+ if !unicode.IsNumber(curr) && curr != '.' {
+ return Split
+ }
+ }
+ // While period is a default delimiter, keep it down here to avoid
+ // penalty for other delimiters
+ if curr == '.' {
+ return SkipSplit
+ }
+
+ return Noop
+}
diff --git a/vendor/github.com/ettle/strcase/strcase.go b/vendor/github.com/ettle/strcase/strcase.go
new file mode 100644
index 0000000000..46b4f7a684
--- /dev/null
+++ b/vendor/github.com/ettle/strcase/strcase.go
@@ -0,0 +1,81 @@
+package strcase
+
+// ToSnake returns words in snake_case (lower case words with underscores).
+func ToSnake(s string) string {
+ return convertWithoutInitialisms(s, '_', LowerCase)
+}
+
+// ToGoSnake returns words in snake_case (lower case words with underscores).
+//
+// Respects Go's common initialisms (e.g. http_response -> HTTP_response).
+func ToGoSnake(s string) string {
+ return convertWithGoInitialisms(s, '_', LowerCase)
+}
+
+// ToSNAKE returns words in SNAKE_CASE (upper case words with underscores).
+// Also known as SCREAMING_SNAKE_CASE or UPPER_CASE.
+func ToSNAKE(s string) string {
+ return convertWithoutInitialisms(s, '_', UpperCase)
+}
+
+// ToKebab returns words in kebab-case (lower case words with dashes).
+// Also known as dash-case.
+func ToKebab(s string) string {
+ return convertWithoutInitialisms(s, '-', LowerCase)
+}
+
+// ToGoKebab returns words in kebab-case (lower case words with dashes).
+// Also known as dash-case.
+//
+// Respects Go's common initialisms (e.g. http-response -> HTTP-response).
+func ToGoKebab(s string) string {
+ return convertWithGoInitialisms(s, '-', LowerCase)
+}
+
+// ToKEBAB returns words in KEBAB-CASE (upper case words with dashes).
+// Also known as SCREAMING-KEBAB-CASE or SCREAMING-DASH-CASE.
+func ToKEBAB(s string) string {
+ return convertWithoutInitialisms(s, '-', UpperCase)
+}
+
+// ToPascal returns words in PascalCase (capitalized words concatenated together).
+// Also known as UpperPascalCase.
+func ToPascal(s string) string {
+ return convertWithoutInitialisms(s, 0, TitleCase)
+}
+
+// ToGoPascal returns words in PascalCase (capitalized words concatenated together).
+// Also known as UpperPascalCase.
+//
+// Respects Go's common initialisms (e.g. HttpResponse -> HTTPResponse).
+func ToGoPascal(s string) string {
+ return convertWithGoInitialisms(s, 0, TitleCase)
+}
+
+// ToCamel returns words in camelCase (capitalized words concatenated together, with first word lower case).
+// Also known as lowerCamelCase or mixedCase.
+func ToCamel(s string) string {
+ return convertWithoutInitialisms(s, 0, CamelCase)
+}
+
+// ToGoCamel returns words in camelCase (capitalized words concatenated together, with first word lower case).
+// Also known as lowerCamelCase or mixedCase.
+//
+// Respects Go's common initialisms, but first word remains lowercased which is
+// important for code generator use cases (e.g. toJson -> toJSON, httpResponse
+// -> httpResponse).
+func ToGoCamel(s string) string {
+ return convertWithGoInitialisms(s, 0, CamelCase)
+}
+
+// ToCase returns words in given case and delimiter.
+func ToCase(s string, wordCase WordCase, delimiter rune) string {
+ return convertWithoutInitialisms(s, delimiter, wordCase)
+}
+
+// ToGoCase returns words in given case and delimiter.
+//
+// Respects Go's common initialisms (e.g. httpResponse -> HTTPResponse).
+func ToGoCase(s string, wordCase WordCase, delimiter rune) string {
+ return convertWithGoInitialisms(s, delimiter, wordCase)
+}
diff --git a/vendor/github.com/ettle/strcase/unicode.go b/vendor/github.com/ettle/strcase/unicode.go
new file mode 100644
index 0000000000..b75e25a512
--- /dev/null
+++ b/vendor/github.com/ettle/strcase/unicode.go
@@ -0,0 +1,48 @@
+package strcase
+
+import "unicode"
+
+// Unicode functions, optimized for the common case of ascii
+// No performance lost by wrapping since these functions get inlined by the compiler
+
+func isUpper(r rune) bool {
+ return unicode.IsUpper(r)
+}
+
+func isLower(r rune) bool {
+ return unicode.IsLower(r)
+}
+
+func isNumber(r rune) bool {
+ if r >= '0' && r <= '9' {
+ return true
+ }
+ return unicode.IsNumber(r)
+}
+
+func isSpace(r rune) bool {
+ if r == ' ' || r == '\t' || r == '\n' || r == '\r' {
+ return true
+ } else if r < 128 {
+ return false
+ }
+ return unicode.IsSpace(r)
+}
+
+func toUpper(r rune) rune {
+ if r >= 'a' && r <= 'z' {
+ return r - 32
+ } else if r < 128 {
+ return r
+ }
+ return unicode.ToUpper(r)
+}
+
+func toLower(r rune) rune {
+ if r >= 'A' && r <= 'Z' {
+ return r + 32
+ } else if r < 128 {
+ return r
+ }
+ return unicode.ToLower(r)
+}
diff --git a/vendor/modules.txt b/vendor/modules.txt
index abe40dee96..6e64145e68 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -108,6 +108,9 @@ github.com/docker/docker-credential-helpers/credentials
## explicit; go 1.13
github.com/emicklei/go-restful/v3
github.com/emicklei/go-restful/v3/log
+# github.com/ettle/strcase v0.1.1
+## explicit; go 1.12
+github.com/ettle/strcase
# github.com/evanphx/json-patch v4.12.0+incompatible
## explicit
github.com/evanphx/json-patch