introduce `internal/helm/registry` package

This new package holds all Helm OCI registry-specific code now so we
have a single location to look for such code which makes it easier to
find yourself around.

Signed-off-by: Max Jonas Werner <mail@makk.es>
This commit is contained in:
Max Jonas Werner 2022-05-23 11:11:53 +02:00
parent ace21c5666
commit c795da2280
No known key found for this signature in database
GPG Key ID: EB525E0F02B52140
7 changed files with 77 additions and 67 deletions

View File

@ -29,7 +29,7 @@ import (
"time" "time"
helmgetter "helm.sh/helm/v3/pkg/getter" helmgetter "helm.sh/helm/v3/pkg/getter"
"helm.sh/helm/v3/pkg/registry" helmreg "helm.sh/helm/v3/pkg/registry"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
apierrs "k8s.io/apimachinery/pkg/api/errors" apierrs "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -64,6 +64,7 @@ import (
sreconcile "github.com/fluxcd/source-controller/internal/reconcile" sreconcile "github.com/fluxcd/source-controller/internal/reconcile"
"github.com/fluxcd/source-controller/internal/reconcile/summarize" "github.com/fluxcd/source-controller/internal/reconcile/summarize"
"github.com/fluxcd/source-controller/internal/util" "github.com/fluxcd/source-controller/internal/util"
"github.com/fluxcd/source-controller/internal/helm/registry"
) )
// helmChartReadyCondition contains all the conditions information // helmChartReadyCondition contains all the conditions information
@ -380,7 +381,7 @@ func (r *HelmChartReconciler) reconcileSource(ctx context.Context, obj *sourcev1
// Assert source has an artifact // Assert source has an artifact
if s.GetArtifact() == nil || !r.Storage.ArtifactExist(*s.GetArtifact()) { if s.GetArtifact() == nil || !r.Storage.ArtifactExist(*s.GetArtifact()) {
if helmRepo, ok := s.(*sourcev1.HelmRepository); !ok || !registry.IsOCI(helmRepo.Spec.URL) { if helmRepo, ok := s.(*sourcev1.HelmRepository); !ok || !helmreg.IsOCI(helmRepo.Spec.URL) {
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, "NoSourceArtifact", conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, "NoSourceArtifact",
"no artifact available for %s source '%s'", obj.Spec.SourceRef.Kind, obj.Spec.SourceRef.Name) "no artifact available for %s source '%s'", obj.Spec.SourceRef.Kind, obj.Spec.SourceRef.Name)
r.eventLogf(ctx, obj, events.EventTypeTrace, "NoSourceArtifact", r.eventLogf(ctx, obj, events.EventTypeTrace, "NoSourceArtifact",
@ -447,7 +448,7 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *
repo *sourcev1.HelmRepository, b *chart.Build) (sreconcile.Result, error) { repo *sourcev1.HelmRepository, b *chart.Build) (sreconcile.Result, error) {
var ( var (
tlsConfig *tls.Config tlsConfig *tls.Config
loginOpts []registry.LoginOption loginOpts []helmreg.LoginOption
) )
// Construct the Getter options from the HelmRepository data // Construct the Getter options from the HelmRepository data
@ -492,7 +493,7 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *
} }
// Build registryClient options from secret // Build registryClient options from secret
loginOpt, err := loginOptionFromSecret(repo.Spec.URL, *secret) loginOpt, err := registry.LoginOptionFromSecret(repo.Spec.URL, *secret)
if err != nil { if err != nil {
e := &serror.Event{ e := &serror.Event{
Err: fmt.Errorf("failed to configure Helm client with secret data: %w", err), Err: fmt.Errorf("failed to configure Helm client with secret data: %w", err),
@ -503,14 +504,14 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *
return sreconcile.ResultEmpty, e return sreconcile.ResultEmpty, e
} }
loginOpts = append([]registry.LoginOption{}, loginOpt) loginOpts = append([]helmreg.LoginOption{}, loginOpt)
} }
// Initialize the chart repository // Initialize the chart repository
var chartRepo chart.Remote var chartRepo chart.Remote
switch repo.Spec.Type { switch repo.Spec.Type {
case sourcev1.HelmRepositoryTypeOCI: case sourcev1.HelmRepositoryTypeOCI:
if !registry.IsOCI(repo.Spec.URL) { if !helmreg.IsOCI(repo.Spec.URL) {
err := fmt.Errorf("invalid OCI registry URL: %s", repo.Spec.URL) err := fmt.Errorf("invalid OCI registry URL: %s", repo.Spec.URL)
return chartRepoErrorReturn(err, obj) return chartRepoErrorReturn(err, obj)
} }
@ -551,7 +552,7 @@ func (r *HelmChartReconciler) buildFromHelmRepository(ctx context.Context, obj *
repository.WithMemoryCache(r.Storage.LocalPath(*repo.GetArtifact()), r.Cache, r.TTL, func(event string) { repository.WithMemoryCache(r.Storage.LocalPath(*repo.GetArtifact()), r.Cache, r.TTL, func(event string) {
r.IncCacheEvents(event, obj.Name, obj.Namespace) r.IncCacheEvents(event, obj.Name, obj.Namespace)
})) }))
if err != nil { if err != nil {
return chartRepoErrorReturn(err, obj) return chartRepoErrorReturn(err, obj)
} }
chartRepo = httpChartRepo chartRepo = httpChartRepo

View File

@ -36,7 +36,7 @@ import (
. "github.com/onsi/gomega" . "github.com/onsi/gomega"
hchart "helm.sh/helm/v3/pkg/chart" hchart "helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chart/loader" "helm.sh/helm/v3/pkg/chart/loader"
"helm.sh/helm/v3/pkg/registry" helmreg "helm.sh/helm/v3/pkg/registry"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors" apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -54,7 +54,7 @@ import (
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
serror "github.com/fluxcd/source-controller/internal/error" serror "github.com/fluxcd/source-controller/internal/error"
"github.com/fluxcd/source-controller/internal/helm/chart" "github.com/fluxcd/source-controller/internal/helm/chart"
"github.com/fluxcd/source-controller/internal/helm/util" "github.com/fluxcd/source-controller/internal/helm/registry"
sreconcile "github.com/fluxcd/source-controller/internal/reconcile" sreconcile "github.com/fluxcd/source-controller/internal/reconcile"
"github.com/fluxcd/source-controller/internal/reconcile/summarize" "github.com/fluxcd/source-controller/internal/reconcile/summarize"
) )
@ -793,8 +793,8 @@ func TestHelmChartReconciler_buildFromOCIHelmRepository(t *testing.T) {
// Login to the registry // Login to the registry
err := testRegistryserver.RegistryClient.Login(testRegistryserver.DockerRegistryHost, err := testRegistryserver.RegistryClient.Login(testRegistryserver.DockerRegistryHost,
registry.LoginOptBasicAuth(testUsername, testPassword), helmreg.LoginOptBasicAuth(testUsername, testPassword),
registry.LoginOptInsecure(true)) helmreg.LoginOptInsecure(true))
g.Expect(err).NotTo(HaveOccurred()) g.Expect(err).NotTo(HaveOccurred())
// Load a test chart // Load a test chart
@ -975,7 +975,7 @@ func TestHelmChartReconciler_buildFromOCIHelmRepository(t *testing.T) {
EventRecorder: record.NewFakeRecorder(32), EventRecorder: record.NewFakeRecorder(32),
Getters: testGetters, Getters: testGetters,
Storage: storage, Storage: storage,
RegistryClientGenerator: util.RegistryClientGenerator, RegistryClientGenerator: registry.ClientGenerator,
} }
repository := &sourcev1.HelmRepository{ repository := &sourcev1.HelmRepository{

View File

@ -17,15 +17,12 @@ limitations under the License.
package controllers package controllers
import ( import (
"bytes"
"context" "context"
"fmt" "fmt"
"net/url"
"os" "os"
"strings" "strings"
"time" "time"
"github.com/docker/cli/cli/config"
"github.com/fluxcd/pkg/apis/meta" "github.com/fluxcd/pkg/apis/meta"
"github.com/fluxcd/pkg/runtime/conditions" "github.com/fluxcd/pkg/runtime/conditions"
helper "github.com/fluxcd/pkg/runtime/controller" helper "github.com/fluxcd/pkg/runtime/controller"
@ -33,12 +30,13 @@ import (
"github.com/fluxcd/pkg/runtime/predicates" "github.com/fluxcd/pkg/runtime/predicates"
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
serror "github.com/fluxcd/source-controller/internal/error" serror "github.com/fluxcd/source-controller/internal/error"
"github.com/fluxcd/source-controller/internal/helm/registry"
"github.com/fluxcd/source-controller/internal/helm/repository" "github.com/fluxcd/source-controller/internal/helm/repository"
intpredicates "github.com/fluxcd/source-controller/internal/predicates" intpredicates "github.com/fluxcd/source-controller/internal/predicates"
sreconcile "github.com/fluxcd/source-controller/internal/reconcile" sreconcile "github.com/fluxcd/source-controller/internal/reconcile"
"github.com/fluxcd/source-controller/internal/reconcile/summarize" "github.com/fluxcd/source-controller/internal/reconcile/summarize"
helmgetter "helm.sh/helm/v3/pkg/getter" helmgetter "helm.sh/helm/v3/pkg/getter"
"helm.sh/helm/v3/pkg/registry" helmreg "helm.sh/helm/v3/pkg/registry"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
kuberecorder "k8s.io/client-go/tools/record" kuberecorder "k8s.io/client-go/tools/record"
@ -94,7 +92,7 @@ type HelmRepositoryOCIReconciler struct {
// and an optional file name. // and an optional file name.
// The file is used to store the registry client credentials. // The file is used to store the registry client credentials.
// The caller is responsible for deleting the file. // The caller is responsible for deleting the file.
type RegistryClientGeneratorFunc func(isLogin bool) (*registry.Client, string, error) type RegistryClientGeneratorFunc func(isLogin bool) (*helmreg.Client, string, error)
// helmRepositoryOCIReconcileFunc is the function type for all the // helmRepositoryOCIReconcileFunc is the function type for all the
// v1beta2.HelmRepository (sub)reconcile functions for OCI type. The type implementations // v1beta2.HelmRepository (sub)reconcile functions for OCI type. The type implementations
@ -257,7 +255,7 @@ func (r *HelmRepositoryOCIReconciler) reconcile(ctx context.Context, obj *source
} }
func (r *HelmRepositoryOCIReconciler) reconcileSource(ctx context.Context, obj *sourcev1.HelmRepository) (sreconcile.Result, error) { func (r *HelmRepositoryOCIReconciler) reconcileSource(ctx context.Context, obj *sourcev1.HelmRepository) (sreconcile.Result, error) {
var loginOpts []registry.LoginOption var loginOpts []helmreg.LoginOption
// Configure any authentication related options // Configure any authentication related options
if obj.Spec.SecretRef != nil { if obj.Spec.SecretRef != nil {
// Attempt to retrieve secret // Attempt to retrieve secret
@ -276,7 +274,7 @@ func (r *HelmRepositoryOCIReconciler) reconcileSource(ctx context.Context, obj *
} }
// Construct actual options // Construct actual options
loginOpt, err := loginOptionFromSecret(obj.Spec.URL, secret) loginOpt, err := registry.LoginOptionFromSecret(obj.Spec.URL, secret)
if err != nil { if err != nil {
e := &serror.Event{ e := &serror.Event{
Err: fmt.Errorf("failed to configure Helm client with secret data: %w", err), Err: fmt.Errorf("failed to configure Helm client with secret data: %w", err),
@ -301,7 +299,7 @@ func (r *HelmRepositoryOCIReconciler) reconcileSource(ctx context.Context, obj *
// validateSource the HelmRepository object by checking the url and connecting to the underlying registry // validateSource the HelmRepository object by checking the url and connecting to the underlying registry
// with he provided credentials. // with he provided credentials.
func (r *HelmRepositoryOCIReconciler) validateSource(ctx context.Context, obj *sourcev1.HelmRepository, logOpts ...registry.LoginOption) (sreconcile.Result, error) { func (r *HelmRepositoryOCIReconciler) validateSource(ctx context.Context, obj *sourcev1.HelmRepository, logOpts ...helmreg.LoginOption) (sreconcile.Result, error) {
registryClient, file, err := r.RegistryClientGenerator(logOpts != nil) registryClient, file, err := r.RegistryClientGenerator(logOpts != nil)
if err != nil { if err != nil {
e := &serror.Stalling{ e := &serror.Stalling{
@ -354,36 +352,3 @@ func (r *HelmRepositoryOCIReconciler) validateSource(ctx context.Context, obj *s
return sreconcile.ResultSuccess, nil return sreconcile.ResultSuccess, nil
} }
// loginOptionFromSecret derives authentication data from a Secret to login to an OCI registry. This Secret
// may either hold "username" and "password" fields or be of the corev1.SecretTypeDockerConfigJson type and hold
// a corev1.DockerConfigJsonKey field with a complete Docker configuration. If both, "username" and "password" are
// empty, a nil LoginOption and a nil error will be returned.
func loginOptionFromSecret(registryURL string, secret corev1.Secret) (registry.LoginOption, error) {
var username, password string
if secret.Type == corev1.SecretTypeDockerConfigJson {
dockerCfg, err := config.LoadFromReader(bytes.NewReader(secret.Data[corev1.DockerConfigJsonKey]))
if err != nil {
return nil, fmt.Errorf("unable to load Docker config: %w", err)
}
parsedURL, err := url.Parse(registryURL)
if err != nil {
return nil, fmt.Errorf("unable to parse registry URL: %w", err)
}
authConfig, err := dockerCfg.GetAuthConfig(parsedURL.Host)
if err != nil {
return nil, fmt.Errorf("unable to get authentication data from Secret: %w", err)
}
username = authConfig.Username
password = authConfig.Password
} else {
username, password = string(secret.Data["username"]), string(secret.Data["password"])
}
switch {
case username == "" && password == "":
return nil, nil
case username == "" || password == "":
return nil, fmt.Errorf("invalid '%s' secret data: required fields 'username' and 'password'", secret.Name)
}
return registry.LoginOptBasicAuth(username, password), nil
}

View File

@ -30,7 +30,7 @@ import (
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
"helm.sh/helm/v3/pkg/getter" "helm.sh/helm/v3/pkg/getter"
"helm.sh/helm/v3/pkg/registry" helmreg "helm.sh/helm/v3/pkg/registry"
utilruntime "k8s.io/apimachinery/pkg/util/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/tools/record" "k8s.io/client-go/tools/record"
@ -49,7 +49,7 @@ import (
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
"github.com/fluxcd/source-controller/internal/cache" "github.com/fluxcd/source-controller/internal/cache"
"github.com/fluxcd/source-controller/internal/features" "github.com/fluxcd/source-controller/internal/features"
"github.com/fluxcd/source-controller/internal/helm/util" "github.com/fluxcd/source-controller/internal/helm/registry"
// +kubebuilder:scaffold:imports // +kubebuilder:scaffold:imports
) )
@ -94,7 +94,7 @@ var (
) )
var ( var (
testRegistryClient *registry.Client testRegistryClient *helmreg.Client
testRegistryserver *RegistryClientTestServer testRegistryserver *RegistryClientTestServer
) )
@ -113,7 +113,7 @@ type RegistryClientTestServer struct {
Out io.Writer Out io.Writer
DockerRegistryHost string DockerRegistryHost string
WorkspaceDir string WorkspaceDir string
RegistryClient *registry.Client RegistryClient *helmreg.Client
} }
func SetupServer(server *RegistryClientTestServer) string { func SetupServer(server *RegistryClientTestServer) string {
@ -129,9 +129,9 @@ func SetupServer(server *RegistryClientTestServer) string {
server.Out = &out server.Out = &out
// init test client // init test client
server.RegistryClient, err = registry.NewClient( server.RegistryClient, err = helmreg.NewClient(
registry.ClientOptDebug(true), helmreg.ClientOptDebug(true),
registry.ClientOptWriter(server.Out), helmreg.ClientOptWriter(server.Out),
) )
if err != nil { if err != nil {
panic(fmt.Sprintf("failed to create registry client: %s", err)) panic(fmt.Sprintf("failed to create registry client: %s", err))
@ -202,7 +202,7 @@ func TestMain(m *testing.M) {
testRegistryserver = &RegistryClientTestServer{} testRegistryserver = &RegistryClientTestServer{}
registryWorkspaceDir := SetupServer(testRegistryserver) registryWorkspaceDir := SetupServer(testRegistryserver)
testRegistryClient, err = registry.NewClient(registry.ClientOptWriter(os.Stdout)) testRegistryClient, err = helmreg.NewClient(helmreg.ClientOptWriter(os.Stdout))
if err != nil { if err != nil {
panic(fmt.Sprintf("Failed to create OCI registry client")) panic(fmt.Sprintf("Failed to create OCI registry client"))
} }
@ -241,7 +241,7 @@ func TestMain(m *testing.M) {
EventRecorder: record.NewFakeRecorder(32), EventRecorder: record.NewFakeRecorder(32),
Metrics: testMetricsH, Metrics: testMetricsH,
Getters: testGetters, Getters: testGetters,
RegistryClientGenerator: util.RegistryClientGenerator, RegistryClientGenerator: registry.ClientGenerator,
}).SetupWithManager(testEnv); err != nil { }).SetupWithManager(testEnv); err != nil {
panic(fmt.Sprintf("Failed to start HelmRepositoryOCIReconciler: %v", err)) panic(fmt.Sprintf("Failed to start HelmRepositoryOCIReconciler: %v", err))
} }

View File

@ -0,0 +1,44 @@
package registry
import (
"bytes"
"fmt"
"net/url"
"github.com/docker/cli/cli/config"
"helm.sh/helm/v3/pkg/registry"
corev1 "k8s.io/api/core/v1"
)
// LoginOptionFromSecret derives authentication data from a Secret to login to an OCI registry. This Secret
// may either hold "username" and "password" fields or be of the corev1.SecretTypeDockerConfigJson type and hold
// a corev1.DockerConfigJsonKey field with a complete Docker configuration. If both, "username" and "password" are
// empty, a nil LoginOption and a nil error will be returned.
func LoginOptionFromSecret(registryURL string, secret corev1.Secret) (registry.LoginOption, error) {
var username, password string
if secret.Type == corev1.SecretTypeDockerConfigJson {
dockerCfg, err := config.LoadFromReader(bytes.NewReader(secret.Data[corev1.DockerConfigJsonKey]))
if err != nil {
return nil, fmt.Errorf("unable to load Docker config: %w", err)
}
parsedURL, err := url.Parse(registryURL)
if err != nil {
return nil, fmt.Errorf("unable to parse registry URL: %w", err)
}
authConfig, err := dockerCfg.GetAuthConfig(parsedURL.Host)
if err != nil {
return nil, fmt.Errorf("unable to get authentication data from Secret: %w", err)
}
username = authConfig.Username
password = authConfig.Password
} else {
username, password = string(secret.Data["username"]), string(secret.Data["password"])
}
switch {
case username == "" && password == "":
return nil, nil
case username == "" || password == "":
return nil, fmt.Errorf("invalid '%s' secret data: required fields 'username' and 'password'", secret.Name)
}
return registry.LoginOptBasicAuth(username, password), nil
}

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
package util package registry
import ( import (
"io" "io"
@ -26,7 +26,7 @@ import (
// RegistryClientGenerator generates a registry client and a temporary credential file. // RegistryClientGenerator generates a registry client and a temporary credential file.
// The client is meant to be used for a single reconciliation. // 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. // The file is meant to be used for a single reconciliation and deleted after.
func RegistryClientGenerator(isLogin bool) (*registry.Client, string, error) { func ClientGenerator(isLogin bool) (*registry.Client, string, error) {
if isLogin { if isLogin {
// create a temporary file to store the credentials // create a temporary file to store the credentials
// this is needed because otherwise the credentials are stored in ~/.docker/config.json. // this is needed because otherwise the credentials are stored in ~/.docker/config.json.

View File

@ -42,7 +42,7 @@ import (
"github.com/fluxcd/pkg/runtime/pprof" "github.com/fluxcd/pkg/runtime/pprof"
"github.com/fluxcd/pkg/runtime/probes" "github.com/fluxcd/pkg/runtime/probes"
"github.com/fluxcd/source-controller/internal/features" "github.com/fluxcd/source-controller/internal/features"
"github.com/fluxcd/source-controller/internal/helm/util" "github.com/fluxcd/source-controller/internal/helm/registry"
sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" sourcev1 "github.com/fluxcd/source-controller/api/v1beta2"
"github.com/fluxcd/source-controller/controllers" "github.com/fluxcd/source-controller/controllers"
@ -239,7 +239,7 @@ func main() {
Metrics: metricsH, Metrics: metricsH,
Getters: getters, Getters: getters,
ControllerName: controllerName, ControllerName: controllerName,
RegistryClientGenerator: util.RegistryClientGenerator, RegistryClientGenerator: registry.ClientGenerator,
}).SetupWithManagerAndOptions(mgr, controllers.HelmRepositoryReconcilerOptions{ }).SetupWithManagerAndOptions(mgr, controllers.HelmRepositoryReconcilerOptions{
MaxConcurrentReconciles: concurrent, MaxConcurrentReconciles: concurrent,
RateLimiter: helper.GetRateLimiter(rateLimiterOptions), RateLimiter: helper.GetRateLimiter(rateLimiterOptions),
@ -270,7 +270,7 @@ func main() {
if err = (&controllers.HelmChartReconciler{ if err = (&controllers.HelmChartReconciler{
Client: mgr.GetClient(), Client: mgr.GetClient(),
RegistryClientGenerator: util.RegistryClientGenerator, RegistryClientGenerator: registry.ClientGenerator,
Storage: storage, Storage: storage,
Getters: getters, Getters: getters,
EventRecorder: eventRecorder, EventRecorder: eventRecorder,