Skip to content

Commit

Permalink
helmrepo: allow OCI helmrepos to connect to insecure registries
Browse files Browse the repository at this point in the history
Signed-off-by: Sanskar Jaiswal <[email protected]>
  • Loading branch information
aryan9600 committed Nov 22, 2023
1 parent 053f485 commit 4b230f3
Show file tree
Hide file tree
Showing 9 changed files with 72 additions and 49 deletions.
14 changes: 10 additions & 4 deletions internal/controller/helmchart_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -548,7 +548,7 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *
// this is needed because otherwise the credentials are stored in ~/.docker/config.json.
// TODO@souleb: remove this once the registry move to Oras v2
// or rework to enable reusing credentials to avoid the unneccessary handshake operations
registryClient, credentialsFile, err := r.RegistryClientGenerator(clientOpts.TlsConfig, clientOpts.MustLoginToRegistry())
registryClient, credentialsFile, err := r.RegistryClientGenerator(clientOpts.TlsConfig, clientOpts.MustLoginToRegistry(), repo.Spec.Insecure)
if err != nil {
e := serror.NewGeneric(
fmt.Errorf("failed to construct Helm client: %w", err),
Expand Down Expand Up @@ -586,11 +586,17 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *

// Tell the chart repository to use the OCI client with the configured getter
getterOpts = append(getterOpts, helmgetter.WithRegistryClient(registryClient))
ociChartRepo, err := repository.NewOCIChartRepository(normalizedURL,
chartRepoOpts := []repository.OCIChartRepositoryOption{
repository.WithOCIGetter(r.Getters),
repository.WithOCIGetterOptions(getterOpts),
repository.WithOCIRegistryClient(registryClient),
repository.WithVerifiers(verifiers))
repository.WithVerifiers(verifiers),
}
if repo.Spec.Insecure {
chartRepoOpts = append(chartRepoOpts, repository.WithInsecureHTTP())
}

ociChartRepo, err := repository.NewOCIChartRepository(normalizedURL, chartRepoOpts...)
if err != nil {
return chartRepoConfigErrorReturn(err, obj)
}
Expand Down Expand Up @@ -1003,7 +1009,7 @@ func (r *HelmChartReconciler) namespacedChartRepositoryCallback(ctx context.Cont

var chartRepo repository.Downloader
if helmreg.IsOCI(normalizedURL) {
registryClient, credentialsFile, err := r.RegistryClientGenerator(clientOpts.TlsConfig, clientOpts.MustLoginToRegistry())
registryClient, credentialsFile, err := r.RegistryClientGenerator(clientOpts.TlsConfig, clientOpts.MustLoginToRegistry(), obj.Spec.Insecure)
if err != nil {
return nil, fmt.Errorf("failed to create registry client: %w", err)
}
Expand Down
28 changes: 19 additions & 9 deletions internal/controller/helmchart_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"errors"
"fmt"
"io"
"net"
"net/http"
"os"
"path"
Expand All @@ -32,6 +33,7 @@ import (
"testing"
"time"

"github.com/foxcpp/go-mockdns"
. "github.com/onsi/gomega"
coptions "github.com/sigstore/cosign/v2/cmd/cosign/cli/options"
"github.com/sigstore/cosign/v2/cmd/cosign/cli/sign"
Expand Down Expand Up @@ -1294,6 +1296,7 @@ func TestHelmChartReconciler_buildFromOCIHelmRepository(t *testing.T) {
Timeout: &metav1.Duration{Duration: timeout},
Provider: helmv1.GenericOCIProvider,
Type: helmv1.HelmRepositoryTypeOCI,
Insecure: true,
},
}
obj := &helmv1.HelmChart{
Expand All @@ -1313,12 +1316,14 @@ func TestHelmChartReconciler_buildFromOCIHelmRepository(t *testing.T) {
}
got, err := r.buildFromHelmRepository(context.TODO(), obj, repository, &b)

g.Expect(err != nil).To(Equal(tt.wantErr != nil))
if tt.wantErr != nil {
g.Expect(err).To(HaveOccurred())
g.Expect(reflect.TypeOf(err).String()).To(Equal(reflect.TypeOf(tt.wantErr).String()))
g.Expect(err.Error()).To(ContainSubstring(tt.wantErr.Error()))
} else {
g.Expect(err).ToNot(HaveOccurred())
g.Expect(got).To(Equal(tt.want))
}
g.Expect(got).To(Equal(tt.want))

if tt.assertFunc != nil {
tt.assertFunc(g, obj, b)
Expand All @@ -1332,6 +1337,14 @@ func TestHelmChartReconciler_buildFromTarballArtifact(t *testing.T) {

tmpDir := t.TempDir()

// Unpatch the changes we make to the default DNS resolver in `setupRegistryServer()`.
// This is required because the changes somehow also cause remote lookups to fail and
// this test tests functionality related to remote dependencies.
mockdns.UnpatchNet(net.DefaultResolver)
defer func() {
testRegistryServer.dnsServer.PatchNet(net.DefaultResolver)
}()

storage, err := NewStorage(tmpDir, "example.com", retentionTTL, retentionRecords)
g.Expect(err).ToNot(HaveOccurred())

Expand Down Expand Up @@ -2429,9 +2442,6 @@ func TestHelmChartReconciler_reconcileSourceFromOCI_authStrategy(t *testing.T) {

workspaceDir := t.TempDir()

if tt.insecure {
tt.registryOpts.disableDNSMocking = true
}
server, err := setupRegistryServer(ctx, workspaceDir, tt.registryOpts)
g.Expect(err).NotTo(HaveOccurred())
t.Cleanup(func() {
Expand All @@ -2456,6 +2466,7 @@ func TestHelmChartReconciler_reconcileSourceFromOCI_authStrategy(t *testing.T) {
Type: helmv1.HelmRepositoryTypeOCI,
Provider: helmv1.GenericOCIProvider,
URL: fmt.Sprintf("oci://%s/testrepo", server.registryHost),
Insecure: tt.insecure,
},
}

Expand Down Expand Up @@ -2725,9 +2736,7 @@ func TestHelmChartReconciler_reconcileSourceFromOCI_verifySignature(t *testing.T
g := NewWithT(t)

tmpDir := t.TempDir()
server, err := setupRegistryServer(ctx, tmpDir, registryOptions{
disableDNSMocking: true,
})
server, err := setupRegistryServer(ctx, tmpDir, registryOptions{})
g.Expect(err).ToNot(HaveOccurred())
t.Cleanup(func() {
server.Close()
Expand Down Expand Up @@ -2870,6 +2879,7 @@ func TestHelmChartReconciler_reconcileSourceFromOCI_verifySignature(t *testing.T
Timeout: &metav1.Duration{Duration: timeout},
Provider: helmv1.GenericOCIProvider,
Type: helmv1.HelmRepositoryTypeOCI,
Insecure: true,
},
}

Expand Down Expand Up @@ -2924,7 +2934,7 @@ func TestHelmChartReconciler_reconcileSourceFromOCI_verifySignature(t *testing.T
Upload: true,
SkipConfirmation: true,
TlogUpload: false,
Registry: coptions.RegistryOptions{Keychain: oci.Anonymous{}, AllowInsecure: true},
Registry: coptions.RegistryOptions{Keychain: oci.Anonymous{}, AllowHTTPRegistry: true},
},
[]string{fmt.Sprintf("%s/testrepo/%s:%s", server.registryHost, metadata.Name, metadata.Version)})
g.Expect(err).ToNot(HaveOccurred())
Expand Down
4 changes: 2 additions & 2 deletions internal/controller/helmrepository_controller_oci.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ type HelmRepositoryOCIReconciler struct {
// and an optional file name.
// The file is used to store the registry client credentials.
// The caller is responsible for deleting the file.
type RegistryClientGeneratorFunc func(tlsConfig *tls.Config, isLogin bool) (*helmreg.Client, string, error)
type RegistryClientGeneratorFunc func(tlsConfig *tls.Config, isLogin, insecure bool) (*helmreg.Client, string, error)

func (r *HelmRepositoryOCIReconciler) SetupWithManager(mgr ctrl.Manager) error {
return r.SetupWithManagerAndOptions(mgr, HelmRepositoryReconcilerOptions{})
Expand Down Expand Up @@ -332,7 +332,7 @@ func (r *HelmRepositoryOCIReconciler) reconcile(ctx context.Context, sp *patch.S
}

// Create registry client and login if needed.
registryClient, file, err := r.RegistryClientGenerator(clientOpts.TlsConfig, clientOpts.MustLoginToRegistry())
registryClient, file, err := r.RegistryClientGenerator(clientOpts.TlsConfig, clientOpts.MustLoginToRegistry(), obj.Spec.Insecure)
if err != nil {
e := fmt.Errorf("failed to create registry client: %w", err)
conditions.MarkFalse(obj, meta.ReadyCondition, meta.FailedReason, e.Error())
Expand Down
4 changes: 1 addition & 3 deletions internal/controller/helmrepository_controller_oci_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -376,9 +376,6 @@ func TestHelmRepositoryOCIReconciler_authStrategy(t *testing.T) {
WithStatusSubresource(&helmv1.HelmRepository{})

workspaceDir := t.TempDir()
if tt.insecure {
tt.registryOpts.disableDNSMocking = true
}
server, err := setupRegistryServer(ctx, workspaceDir, tt.registryOpts)
g.Expect(err).NotTo(HaveOccurred())
t.Cleanup(func() {
Expand All @@ -396,6 +393,7 @@ func TestHelmRepositoryOCIReconciler_authStrategy(t *testing.T) {
Type: helmv1.HelmRepositoryTypeOCI,
Provider: helmv1.GenericOCIProvider,
URL: fmt.Sprintf("oci://%s", server.registryHost),
Insecure: tt.insecure,
},
}

Expand Down
38 changes: 15 additions & 23 deletions internal/controller/suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,11 +128,6 @@ type registryOptions struct {
withBasicAuth bool
withTLS bool
withClientCertAuth bool
// Allow disbaling DNS mocking since Helm OCI doesn't yet suppot
// insecure OCI registries, which means we need Docker's automatic
// connection downgrading if the registry is hosted on localhost.
// Once Helm OCI supports insecure registries, we can get rid of this.
disableDNSMocking bool
}

func setupRegistryServer(ctx context.Context, workspaceDir string, opts registryOptions) (*registryClientTestServer, error) {
Expand All @@ -159,27 +154,23 @@ func setupRegistryServer(ctx context.Context, workspaceDir string, opts registry
return nil, fmt.Errorf("failed to get free port: %s", err)
}

server.registryHost = fmt.Sprintf("localhost:%d", port)

// Change the registry host to a host which is not localhost and
// mock DNS to map example.com to 127.0.0.1.
// This is required because Docker enforces HTTP if the registry
// is hosted on localhost/127.0.0.1.
if !opts.disableDNSMocking {
server.registryHost = fmt.Sprintf("example.com:%d", port)
// Disable DNS server logging as it is extremely chatty.
dnsLog := log.Default()
dnsLog.SetOutput(io.Discard)
server.dnsServer, err = mockdns.NewServerWithLogger(map[string]mockdns.Zone{
"example.com.": {
A: []string{"127.0.0.1"},
},
}, dnsLog, false)
if err != nil {
return nil, err
}
server.dnsServer.PatchNet(net.DefaultResolver)
server.registryHost = fmt.Sprintf("example.com:%d", port)
// Disable DNS server logging as it is extremely chatty.
dnsLog := log.Default()
dnsLog.SetOutput(io.Discard)
server.dnsServer, err = mockdns.NewServerWithLogger(map[string]mockdns.Zone{
"example.com.": {
A: []string{"127.0.0.1"},
},
}, dnsLog, false)
if err != nil {
return nil, err
}
server.dnsServer.PatchNet(net.DefaultResolver)

config.HTTP.Addr = fmt.Sprintf(":%d", port)
config.HTTP.DrainTimeout = time.Duration(10) * time.Second
Expand Down Expand Up @@ -220,6 +211,8 @@ func setupRegistryServer(ctx context.Context, workspaceDir string, opts registry
return nil, fmt.Errorf("failed to create TLS configured HTTP client: %s", err)
}
clientOpts = append(clientOpts, helmreg.ClientOptHTTPClient(httpClient))
} else {
clientOpts = append(clientOpts, helmreg.ClientOptPlainHTTP())
}

// setup logger options
Expand Down Expand Up @@ -313,8 +306,7 @@ func TestMain(m *testing.M) {
panic(fmt.Sprintf("failed to create workspace directory: %v", err))
}
testRegistryServer, err = setupRegistryServer(ctx, testWorkspaceDir, registryOptions{
withBasicAuth: true,
disableDNSMocking: true,
withBasicAuth: true,
})
if err != nil {
panic(fmt.Sprintf("Failed to create a test registry server: %v", err))
Expand Down
1 change: 0 additions & 1 deletion internal/helm/getter/client_opts.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,6 @@ func GetClientOpts(ctx context.Context, c client.Client, obj *helmv1.HelmReposit
helmgetter.WithURL(url),
helmgetter.WithTimeout(obj.Spec.Timeout.Duration),
helmgetter.WithPassCredentialsAll(obj.Spec.PassCredentials),
helmgetter.WithPlainHTTP(obj.Spec.Insecure),
},
}
ociRepo := obj.Spec.Type == helmv1.HelmRepositoryTypeOCI
Expand Down
4 changes: 2 additions & 2 deletions internal/helm/getter/client_opts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ func TestGetClientOpts(t *testing.T) {
},
afterFunc: func(t *WithT, hcOpts *ClientOpts) {
t.Expect(hcOpts.TlsConfig).ToNot(BeNil())
t.Expect(len(hcOpts.GetterOpts)).To(Equal(5))
t.Expect(len(hcOpts.GetterOpts)).To(Equal(4))
},
},
{
Expand All @@ -85,7 +85,7 @@ func TestGetClientOpts(t *testing.T) {
},
afterFunc: func(t *WithT, hcOpts *ClientOpts) {
t.Expect(hcOpts.TlsConfig).ToNot(BeNil())
t.Expect(len(hcOpts.GetterOpts)).To(Equal(5))
t.Expect(len(hcOpts.GetterOpts)).To(Equal(4))
},
err: ErrDeprecatedTLSConfig,
},
Expand Down
11 changes: 7 additions & 4 deletions internal/helm/registry/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import (
// ClientGenerator generates a registry client and a temporary credential file.
// The client is meant to be used for a single reconciliation.
// The file is meant to be used for a single reconciliation and deleted after.
func ClientGenerator(tlsConfig *tls.Config, isLogin bool) (*registry.Client, string, error) {
func ClientGenerator(tlsConfig *tls.Config, isLogin, insecureHTTP bool) (*registry.Client, string, error) {
if isLogin {
// create a temporary file to store the credentials
// this is needed because otherwise the credentials are stored in ~/.docker/config.json.
Expand All @@ -39,7 +39,7 @@ func ClientGenerator(tlsConfig *tls.Config, isLogin bool) (*registry.Client, str
}

var errs []error
rClient, err := newClient(credentialsFile.Name(), tlsConfig)
rClient, err := newClient(credentialsFile.Name(), tlsConfig, insecureHTTP)
if err != nil {
errs = append(errs, err)
// attempt to delete the temporary file
Expand All @@ -54,17 +54,20 @@ func ClientGenerator(tlsConfig *tls.Config, isLogin bool) (*registry.Client, str
return rClient, credentialsFile.Name(), nil
}

rClient, err := newClient("", tlsConfig)
rClient, err := newClient("", tlsConfig, insecureHTTP)
if err != nil {
return nil, "", err
}
return rClient, "", nil
}

func newClient(credentialsFile string, tlsConfig *tls.Config) (*registry.Client, error) {
func newClient(credentialsFile string, tlsConfig *tls.Config, insecureHTTP bool) (*registry.Client, error) {
opts := []registry.ClientOption{
registry.ClientOptWriter(io.Discard),
}
if insecureHTTP {
opts = append(opts, registry.ClientOptPlainHTTP())
}
if tlsConfig != nil {
opts = append(opts, registry.ClientOptHTTPClient(&http.Client{
Transport: &http.Transport{
Expand Down
17 changes: 16 additions & 1 deletion internal/helm/repository/oci_chart_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ type OCIChartRepository struct {

// verifiers is a list of verifiers to use when verifying a chart.
verifiers []oci.Verifier

// insecureHTTP indicates that the chart is hosted on an insecure registry.
insecure bool
}

// OCIChartRepositoryOption is a function that can be passed to NewOCIChartRepository
Expand All @@ -89,6 +92,13 @@ func WithVerifiers(verifiers []oci.Verifier) OCIChartRepositoryOption {
}
}

func WithInsecureHTTP() OCIChartRepositoryOption {
return func(r *OCIChartRepository) error {
r.insecure = true
return nil
}
}

// WithOCIRegistryClient returns a ChartRepositoryOption that will set the registry client
func WithOCIRegistryClient(client RegistryClient) OCIChartRepositoryOption {
return func(r *OCIChartRepository) error {
Expand Down Expand Up @@ -358,7 +368,12 @@ func (r *OCIChartRepository) VerifyChart(ctx context.Context, chart *repo.ChartV
return fmt.Errorf("chart '%s' has no downloadable URLs", chart.Name)
}

ref, err := name.ParseReference(strings.TrimPrefix(chart.URLs[0], fmt.Sprintf("%s://", registry.OCIScheme)))
var nameOpts []name.Option
if r.insecure {
nameOpts = append(nameOpts, name.Insecure)
}

ref, err := name.ParseReference(strings.TrimPrefix(chart.URLs[0], fmt.Sprintf("%s://", registry.OCIScheme)), nameOpts...)
if err != nil {
return fmt.Errorf("invalid chart reference: %s", err)
}
Expand Down

0 comments on commit 4b230f3

Please sign in to comment.