diff --git a/cmd/helm/push_test.go b/cmd/helm/push_test.go index 8e56d99dc..1a85f2036 100644 --- a/cmd/helm/push_test.go +++ b/cmd/helm/push_test.go @@ -17,7 +17,23 @@ limitations under the License. package main import ( + "bytes" + "context" + "errors" + "fmt" + "io" + "net" + "os" "testing" + "time" + + "helm.sh/helm/v3/pkg/action" + helmRegistry "helm.sh/helm/v3/pkg/registry" + + "github.com/distribution/distribution/v3/configuration" + "github.com/distribution/distribution/v3/registry" + "github.com/phayes/freeport" + "github.com/stretchr/testify/suite" ) func TestPushFileCompletion(t *testing.T) { @@ -25,3 +41,121 @@ func TestPushFileCompletion(t *testing.T) { checkFileCompletion(t, "push package.tgz", false) checkFileCompletion(t, "push package.tgz oci://localhost:5000", false) } + +const ( + testWorkspaceDir = "helm-registry-test" +) + +type HelmPushTestSuite struct { + suite.Suite + Out io.Writer + DockerRegistryHost string + CompromisedRegistryHost string + WorkspaceDir string + RegistryClient *helmRegistry.Client + PushClient *action.Push +} + +func getLocalIP() (string, error) { + addrs, err := net.InterfaceAddrs() + + if err != nil { + return "", err + } + for _, address := range addrs { + // filter loopback ip + if ipNet, ok := address.(*net.IPNet); ok && !ipNet.IP.IsLoopback() { + if ipNet.IP.To4() != nil { + return ipNet.IP.String(), nil + } + } + } + + return "", errors.New("can not find the client ip address") + +} + +func TestGetLocalIP(t *testing.T) { + ip, err := getLocalIP() + if err != nil { + t.Errorf("get local ip error %+v", err) + } + t.Log(ip) +} + +func (suite *HelmPushTestSuite) SetupSuite() { + suite.WorkspaceDir = testWorkspaceDir + os.RemoveAll(suite.WorkspaceDir) + os.Mkdir(suite.WorkspaceDir, 0700) + + var out bytes.Buffer + suite.Out = &out + + // Registry config + config := &configuration.Configuration{} + port, err := freeport.GetFreePort() + suite.Nil(err, "no error finding free port for test registry") + + ip, err := getLocalIP() + suite.Nil(err, "get host ip error") + suite.DockerRegistryHost = fmt.Sprintf("%s:%d", ip, port) + config.HTTP.Addr = fmt.Sprintf("%s:%d", ip, port) + config.HTTP.DrainTimeout = time.Duration(10) * time.Second + config.Storage = map[string]configuration.Parameters{"inmemory": map[string]interface{}{}} + + dockerRegistry, err := registry.NewRegistry(context.Background(), config) + suite.Nil(err, "no error creating test registry") + + suite.CompromisedRegistryHost = fmt.Sprintf("%s:%d", ip, port) + + // Start Docker registry + go dockerRegistry.ListenAndServe() +} + +func (suite *HelmPushTestSuite) TestSecretPush() { + // init test client + var err error + suite.RegistryClient, err = helmRegistry.NewClient( + helmRegistry.ClientOptDebug(true), + helmRegistry.ClientOptEnableCache(true), + helmRegistry.ClientOptWriter(suite.Out), + ) + suite.Nil(err, "no error creating registry client") + + // init push client + cfg := &action.Configuration{ + RegistryClient: suite.RegistryClient, + } + suite.PushClient = action.NewPushWithOpts(action.WithPushConfig(cfg)) + ref := fmt.Sprintf("oci://%s/testrepo", suite.DockerRegistryHost) + // Load a test chart + _, err = suite.PushClient.Run("./testdata/testcharts/examplechart-0.1.0.tgz", ref) + suite.ErrorContains(err, "http: server gave HTTP response to HTTPS client") +} + +func (suite *HelmPushTestSuite) TestInsecretPush() { + // init test client + var err error + suite.RegistryClient, err = helmRegistry.NewClient( + helmRegistry.ClientOptDebug(true), + helmRegistry.ClientOptEnableCache(true), + helmRegistry.ClientOptWriter(suite.Out), + helmRegistry.ClientPlainHTTP(), + ) + suite.Nil(err, "no error creating registry client") + + // init push client + cfg := &action.Configuration{ + RegistryClient: suite.RegistryClient, + } + suite.PushClient = action.NewPushWithOpts(action.WithPushConfig(cfg)) + ref := fmt.Sprintf("oci://%s/testrepo", suite.DockerRegistryHost) + // Load a test chart + _, err = suite.PushClient.Run("./testdata/testcharts/examplechart-0.1.0.tgz", ref) + suite.Nil(err, "no error loading test chart") + +} + +func TestHelmPushTestSuite(t *testing.T) { + suite.Run(t, new(HelmPushTestSuite)) +} diff --git a/cmd/helm/root.go b/cmd/helm/root.go index ef92fea92..a9a6ef609 100644 --- a/cmd/helm/root.go +++ b/cmd/helm/root.go @@ -152,12 +152,16 @@ func newRootCmd(actionConfig *action.Configuration, out io.Writer, args []string flags.ParseErrorsWhitelist.UnknownFlags = true flags.Parse(args) - registryClient, err := registry.NewClient( + clientOptions := []registry.ClientOption{ registry.ClientOptDebug(settings.Debug), registry.ClientOptEnableCache(true), registry.ClientOptWriter(out), registry.ClientOptCredentialsFile(settings.RegistryConfig), - ) + } + if settings.RegistryPlainHTTP { + clientOptions = append(clientOptions, registry.ClientPlainHTTP()) + } + registryClient, err := registry.NewClient(clientOptions...) if err != nil { return nil, err } diff --git a/cmd/helm/testdata/testcharts/examplechart-0.1.0.tgz b/cmd/helm/testdata/testcharts/examplechart-0.1.0.tgz new file mode 100644 index 000000000..c5ea741eb Binary files /dev/null and b/cmd/helm/testdata/testcharts/examplechart-0.1.0.tgz differ diff --git a/pkg/cli/environment.go b/pkg/cli/environment.go index ac3093629..302001a50 100644 --- a/pkg/cli/environment.go +++ b/pkg/cli/environment.go @@ -80,6 +80,8 @@ type EnvSettings struct { MaxHistory int // BurstLimit is the default client-side throttling limit. BurstLimit int + // RegistryPlainHTTP is set transport data to registry use http transport schema + RegistryPlainHTTP bool } func New() *EnvSettings { @@ -99,6 +101,7 @@ func New() *EnvSettings { RepositoryConfig: envOr("HELM_REPOSITORY_CONFIG", helmpath.ConfigPath("repositories.yaml")), RepositoryCache: envOr("HELM_REPOSITORY_CACHE", helmpath.CachePath("repository")), BurstLimit: envIntOr("HELM_BURST_LIMIT", defaultBurstLimit), + RegistryPlainHTTP: envBoolOr("HELM_REGISTRY_PLAIN_HTTP", false), } env.Debug, _ = strconv.ParseBool(os.Getenv("HELM_DEBUG")) @@ -139,6 +142,8 @@ func (s *EnvSettings) AddFlags(fs *pflag.FlagSet) { fs.StringVar(&s.RepositoryConfig, "repository-config", s.RepositoryConfig, "path to the file containing repository names and URLs") fs.StringVar(&s.RepositoryCache, "repository-cache", s.RepositoryCache, "path to the file containing cached repository indexes") fs.IntVar(&s.BurstLimit, "burst-limit", s.BurstLimit, "client-side default throttling limit") + fs.BoolVar(&s.RegistryPlainHTTP, "registry-plainhttp", s.RegistryPlainHTTP, "set client data use http transport protocol to registry server") + } func envOr(name, def string) string { diff --git a/pkg/cli/environment_test.go b/pkg/cli/environment_test.go index dbf056e3a..7a199651f 100644 --- a/pkg/cli/environment_test.go +++ b/pkg/cli/environment_test.go @@ -48,15 +48,16 @@ func TestEnvSettings(t *testing.T) { envvars map[string]string // expected values - ns, kcontext string - debug bool - maxhistory int - kubeAsUser string - kubeAsGroups []string - kubeCaFile string - kubeInsecure bool - kubeTLSServer string - burstLimit int + ns, kcontext string + debug bool + maxhistory int + kubeAsUser string + kubeAsGroups []string + kubeCaFile string + kubeInsecure bool + kubeTLSServer string + burstLimit int + registryInsecure bool }{ { name: "defaults", @@ -78,31 +79,33 @@ func TestEnvSettings(t *testing.T) { kubeInsecure: true, }, { - name: "with envvars set", - envvars: map[string]string{"HELM_DEBUG": "1", "HELM_NAMESPACE": "yourns", "HELM_KUBEASUSER": "pikachu", "HELM_KUBEASGROUPS": ",,,operators,snackeaters,partyanimals", "HELM_MAX_HISTORY": "5", "HELM_KUBECAFILE": "/tmp/ca.crt", "HELM_BURST_LIMIT": "150", "HELM_KUBEINSECURE_SKIP_TLS_VERIFY": "true", "HELM_KUBETLS_SERVER_NAME": "example.org"}, - ns: "yourns", - maxhistory: 5, - burstLimit: 150, - debug: true, - kubeAsUser: "pikachu", - kubeAsGroups: []string{"operators", "snackeaters", "partyanimals"}, - kubeCaFile: "/tmp/ca.crt", - kubeTLSServer: "example.org", - kubeInsecure: true, + name: "with envvars set", + envvars: map[string]string{"HELM_DEBUG": "1", "HELM_NAMESPACE": "yourns", "HELM_KUBEASUSER": "pikachu", "HELM_KUBEASGROUPS": ",,,operators,snackeaters,partyanimals", "HELM_MAX_HISTORY": "5", "HELM_KUBECAFILE": "/tmp/ca.crt", "HELM_BURST_LIMIT": "150", "HELM_KUBEINSECURE_SKIP_TLS_VERIFY": "true", "HELM_KUBETLS_SERVER_NAME": "example.org", "HELM_REGISTRY_PLAIN_HTTP": "true"}, + ns: "yourns", + maxhistory: 5, + burstLimit: 150, + debug: true, + kubeAsUser: "pikachu", + kubeAsGroups: []string{"operators", "snackeaters", "partyanimals"}, + kubeCaFile: "/tmp/ca.crt", + kubeTLSServer: "example.org", + kubeInsecure: true, + registryInsecure: true, }, { - name: "with flags and envvars set", - args: "--debug --namespace=myns --kube-as-user=poro --kube-as-group=admins --kube-as-group=teatime --kube-as-group=snackeaters --kube-ca-file=/my/ca.crt --burst-limit 175 --kube-insecure-skip-tls-verify=true --kube-tls-server-name=example.org", - envvars: map[string]string{"HELM_DEBUG": "1", "HELM_NAMESPACE": "yourns", "HELM_KUBEASUSER": "pikachu", "HELM_KUBEASGROUPS": ",,,operators,snackeaters,partyanimals", "HELM_MAX_HISTORY": "5", "HELM_KUBECAFILE": "/tmp/ca.crt", "HELM_BURST_LIMIT": "200", "HELM_KUBEINSECURE_SKIP_TLS_VERIFY": "true", "HELM_KUBETLS_SERVER_NAME": "example.org"}, - ns: "myns", - debug: true, - maxhistory: 5, - burstLimit: 175, - kubeAsUser: "poro", - kubeAsGroups: []string{"admins", "teatime", "snackeaters"}, - kubeCaFile: "/my/ca.crt", - kubeTLSServer: "example.org", - kubeInsecure: true, + name: "with flags and envvars set", + args: "--debug --namespace=myns --kube-as-user=poro --kube-as-group=admins --kube-as-group=teatime --kube-as-group=snackeaters --kube-ca-file=/my/ca.crt --burst-limit 175 --kube-insecure-skip-tls-verify=true --kube-tls-server-name=example.org", + envvars: map[string]string{"HELM_DEBUG": "1", "HELM_NAMESPACE": "yourns", "HELM_KUBEASUSER": "pikachu", "HELM_KUBEASGROUPS": ",,,operators,snackeaters,partyanimals", "HELM_MAX_HISTORY": "5", "HELM_KUBECAFILE": "/tmp/ca.crt", "HELM_BURST_LIMIT": "200", "HELM_KUBEINSECURE_SKIP_TLS_VERIFY": "true", "HELM_KUBETLS_SERVER_NAME": "example.org", "HELM_REGISTRY_PLAIN_HTTP": "true"}, + ns: "myns", + debug: true, + maxhistory: 5, + burstLimit: 175, + kubeAsUser: "poro", + kubeAsGroups: []string{"admins", "teatime", "snackeaters"}, + kubeCaFile: "/my/ca.crt", + kubeTLSServer: "example.org", + kubeInsecure: true, + registryInsecure: true, }, } @@ -150,6 +153,9 @@ func TestEnvSettings(t *testing.T) { if tt.kubeTLSServer != settings.KubeTLSServerName { t.Errorf("expected kubeTLSServer %q, got %q", tt.kubeTLSServer, settings.KubeTLSServerName) } + if tt.registryInsecure != settings.RegistryPlainHTTP { + t.Errorf("expected registryInsecure %t, got %t", tt.registryInsecure, settings.RegistryPlainHTTP) + } }) } } diff --git a/pkg/registry/client.go b/pkg/registry/client.go index c1004f956..df3986871 100644 --- a/pkg/registry/client.go +++ b/pkg/registry/client.go @@ -61,6 +61,7 @@ type ( authorizer auth.Client registryAuthorizer *registryauth.Client resolver remotes.Resolver + PlainHTTP bool } // ClientOption allows specifying various settings configurable by the user for overriding the defaults @@ -90,6 +91,9 @@ func NewClient(options ...ClientOption) (*Client, error) { headers := http.Header{} headers.Set("User-Agent", version.GetUserAgent()) opts := []auth.ResolverOption{auth.WithResolverHeaders(headers)} + if client.PlainHTTP { + opts = append(opts, auth.WithResolverPlainHTTP()) + } resolver, err := client.authorizer.ResolverWithOpts(opts...) if err != nil { return nil, err @@ -166,6 +170,13 @@ func ClientOptCredentialsFile(credentialsFile string) ClientOption { } } +// ClientPlainHTTP returns a function that sets the PlainHTTP setting to true on resolver. use http schema transport data +func ClientPlainHTTP() ClientOption { + return func(client *Client) { + client.PlainHTTP = true + } +} + type ( // LoginOption allows specifying various settings on login LoginOption func(*loginOperation)