diff --git a/.golangci.yaml b/.golangci.yaml index 6f28a189a2..8a105b4382 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -109,9 +109,9 @@ linters-settings: - prefix(github.com/open-component-model/ocm) custom-order: true staticcheck: - go: "1.18" + go: "1.21" stylecheck: - go: "1.18" + go: "1.21" funlen: lines: 110 statements: 60 diff --git a/cmds/ocm/commands/controllercmds/install/cmd.go b/cmds/ocm/commands/controllercmds/install/cmd.go index dc59f25e74..e17d8379fb 100644 --- a/cmds/ocm/commands/controllercmds/install/cmd.go +++ b/cmds/ocm/commands/controllercmds/install/cmd.go @@ -18,7 +18,11 @@ import ( "github.com/mandelsoft/filepath/pkg/filepath" "github.com/spf13/cobra" "github.com/spf13/pflag" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/client" "github.com/open-component-model/ocm/cmds/ocm/commands/controllercmds/names" "github.com/open-component-model/ocm/cmds/ocm/commands/verbs" @@ -34,13 +38,18 @@ var ( type Command struct { utils.BaseCommand - Namespace string - ControllerName string - Timeout time.Duration - Version string - BaseURL string - ReleaseAPIURL string - DryRun bool + Namespace string + ControllerName string + Timeout time.Duration + Version string + BaseURL string + ReleaseAPIURL string + CertManagerBaseURL string + CertManagerReleaseAPIURL string + CertManagerVersion string + DryRun bool + SkipPreFlightCheck bool + InstallPrerequisites bool } var _ utils.OCMCommand = (*Command)(nil) @@ -53,7 +62,7 @@ func NewCommand(ctx clictx.Context, names ...string) *cobra.Command { func (o *Command) ForName(name string) *cobra.Command { return &cobra.Command{ Use: "install controller {--version v0.0.1}", - Short: "Install either a specific or latest version of the ocm-controller.", + Short: "Install either a specific or latest version of the ocm-controller. Optionally install prerequisites required by the controller.", } } @@ -61,10 +70,15 @@ func (o *Command) AddFlags(set *pflag.FlagSet) { set.StringVarP(&o.Version, "version", "v", "latest", "the version of the controller to install") set.StringVarP(&o.BaseURL, "base-url", "u", "https://github.com/open-component-model/ocm-controller/releases", "the base url to the ocm-controller's release page") set.StringVarP(&o.ReleaseAPIURL, "release-api-url", "a", "https://api.github.com/repos/open-component-model/ocm-controller/releases", "the base url to the ocm-controller's API release page") + set.StringVar(&o.CertManagerBaseURL, "cert-manager-base-url", "https://github.com/cert-manager/cert-manager/releases", "the base url to the cert-manager's release page") + set.StringVar(&o.CertManagerReleaseAPIURL, "cert-manager-release-api-url", "https://api.github.com/repos/cert-manager/cert-manager/releases", "the base url to the cert-manager's API release page") + set.StringVar(&o.CertManagerVersion, "cert-manager-version", "v1.13.2", "version for cert-manager") set.StringVarP(&o.ControllerName, "controller-name", "c", "ocm-controller", "name of the controller that's used for status check") set.StringVarP(&o.Namespace, "namespace", "n", "ocm-system", "the namespace into which the controller is installed") set.DurationVarP(&o.Timeout, "timeout", "t", 1*time.Minute, "maximum time to wait for deployment to be ready") set.BoolVarP(&o.DryRun, "dry-run", "d", false, "if enabled, prints the downloaded manifest file") + set.BoolVarP(&o.SkipPreFlightCheck, "skip-pre-flight-check", "s", false, "skip the pre-flight check for clusters") + set.BoolVarP(&o.InstallPrerequisites, "install-prerequisites", "i", true, "install prerequisites required by ocm-controller") } func (o *Command) Complete(args []string) error { @@ -72,17 +86,50 @@ func (o *Command) Complete(args []string) error { } func (o *Command) Run() error { + ctx := context.Background() + if !o.SkipPreFlightCheck { + out.Outf(o.Context, "► running pre-install check\n") + if err := o.RunPreFlightCheck(ctx); err != nil { + if o.InstallPrerequisites { + out.Outf(o.Context, "► installing prerequisites\n") + if err := o.installPrerequisites(ctx); err != nil { + return err + } + + out.Outf(o.Context, "✔ successfully installed prerequisites\n") + } else { + return fmt.Errorf("✗ failed to run pre-flight check: %w\n", err) + } + } + } + out.Outf(o.Context, "► installing ocm-controller with version %s\n", o.Version) version := o.Version + if err := o.installManifest( + ctx, + o.ReleaseAPIURL, + o.BaseURL, + "ocm-controller", + "install.yaml", + version, + ); err != nil { + return err + } + + out.Outf(o.Context, "✔ ocm-controller successfully installed\n") + return nil +} + +func (o *Command) installManifest(ctx context.Context, releaseURL, baseURL, manifest, filename, version string) error { if version == "latest" { - latest, err := o.GetLatestVersion() + latest, err := o.getLatestVersion(ctx, releaseURL) if err != nil { - return fmt.Errorf("✗ failed to retrieve latest version for ocm-controller: %s", err) + return fmt.Errorf("✗ failed to retrieve latest version for %s: %w", manifest, err) } out.Outf(o.Context, "► got latest version %q\n", latest) version = latest } else { - exists, err := o.ExistingVersion(version) + exists, err := o.existingVersion(ctx, releaseURL, version) if err != nil { return fmt.Errorf("✗ failed to check if version exists: %w", err) } @@ -91,25 +138,25 @@ func (o *Command) Run() error { } } - temp, err := os.MkdirTemp("", "ocm-controller-download") + temp, err := os.MkdirTemp("", manifest+"-download") if err != nil { return fmt.Errorf("✗ failed to create temp folder: %w", err) } defer os.RemoveAll(temp) - if err := o.fetch(context.Background(), version, temp); err != nil { + if err := o.fetch(ctx, baseURL, version, temp, filename); err != nil { return fmt.Errorf("✗ failed to download install.yaml file: %w", err) } - path := filepath.Join(temp, "install.yaml") + path := filepath.Join(temp, filename) if _, err := os.Stat(path); os.IsNotExist(err) { - return fmt.Errorf("✗ failed to find install.yaml file at location: %w", err) + return fmt.Errorf("✗ failed to find %s file at location: %w", filename, err) } out.Outf(o.Context, "✔ successfully fetched install file\n") if o.DryRun { content, err := os.ReadFile(path) if err != nil { - return fmt.Errorf("✗ failed to read install.yaml file at location: %w", err) + return fmt.Errorf("✗ failed to read %s file at location: %w", filename, err) } out.Outf(o.Context, string(content)) return nil @@ -136,16 +183,17 @@ func (o *Command) Run() error { return fmt.Errorf("✗ failed to wait for objects to be ready: %w", err) } - out.Outf(o.Context, "✔ ocm-controller successfully installed\n") return nil } -// GetLatestVersion calls the GitHub API and returns the latest released version. -func (o *Command) GetLatestVersion() (string, error) { - c := http.DefaultClient - c.Timeout = 15 * time.Second +// getLatestVersion calls the GitHub API and returns the latest released version. +func (o *Command) getLatestVersion(ctx context.Context, url string) (string, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url+"/latest", nil) + if err != nil { + return "", err + } - res, err := c.Get(o.ReleaseAPIURL + "/latest") + res, err := http.DefaultClient.Do(req) if err != nil { return "", fmt.Errorf("GitHub API call failed: %w", err) } @@ -165,21 +213,23 @@ func (o *Command) GetLatestVersion() (string, error) { return m.Tag, err } -// ExistingVersion calls the GitHub API to confirm the given version does exist. -func (o *Command) ExistingVersion(version string) (bool, error) { +// existingVersion calls the GitHub API to confirm the given version does exist. +func (o *Command) existingVersion(ctx context.Context, url, version string) (bool, error) { if !strings.HasPrefix(version, "v") { version = "v" + version } - ghURL := fmt.Sprintf(o.ReleaseAPIURL+"/tags/%s", version) - c := http.DefaultClient - c.Timeout = 15 * time.Second - - res, err := c.Get(ghURL) + ghURL := fmt.Sprintf(url+"/tags/%s", version) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, ghURL, nil) if err != nil { return false, fmt.Errorf("GitHub API call failed: %w", err) } + res, err := http.DefaultClient.Do(req) + if err != nil { + return false, err + } + if res.Body != nil { defer res.Body.Close() } @@ -194,18 +244,17 @@ func (o *Command) ExistingVersion(version string) (bool, error) { } } -func (o *Command) fetch(ctx context.Context, version, dir string) error { - ghURL := fmt.Sprintf("%s/latest/download/install.yaml", o.BaseURL) +func (o *Command) fetch(ctx context.Context, url, version, dir, filename string) error { + ghURL := fmt.Sprintf("%s/latest/download/%s", url, filename) if strings.HasPrefix(version, "v") { - ghURL = fmt.Sprintf("%s/download/%s/install.yaml", o.BaseURL, version) + ghURL = fmt.Sprintf("%s/download/%s/%s", url, version, filename) } - req, err := http.NewRequest("GET", ghURL, nil) + req, err := http.NewRequest(http.MethodGet, ghURL, nil) if err != nil { return fmt.Errorf("failed to create HTTP request for %s, error: %w", ghURL, err) } - // download resp, err := http.DefaultClient.Do(req.WithContext(ctx)) if err != nil { return fmt.Errorf("failed to download manifests.tar.gz from %s, error: %w", ghURL, err) @@ -214,10 +263,10 @@ func (o *Command) fetch(ctx context.Context, version, dir string) error { // check response if resp.StatusCode != http.StatusOK { - return fmt.Errorf("failed to download manifests.tar.gz from %s, status: %s", ghURL, resp.Status) + return fmt.Errorf("failed to download %s from %s, status: %s", filename, ghURL, resp.Status) } - wf, err := os.OpenFile(filepath.Join(dir, "install.yaml"), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0777) + wf, err := os.OpenFile(filepath.Join(dir, filename), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o777) if err != nil { return fmt.Errorf("failed to open temp file: %w", err) } @@ -228,3 +277,63 @@ func (o *Command) fetch(ctx context.Context, version, dir string) error { return nil } + +// RunPreFlightCheck checks if the target cluster has the following items: +// - secret containing certificates for the in-cluster registry +// - flux installed. +func (o *Command) RunPreFlightCheck(ctx context.Context) error { + rcg := genericclioptions.NewConfigFlags(false) + cfg, err := rcg.ToRESTConfig() + if err != nil { + return fmt.Errorf("loading kubeconfig failed: %w", err) + } + + // bump limits + cfg.QPS = 100.0 + cfg.Burst = 300 + + if err := o.checkCertificateSecretExists(ctx, cfg, rcg); err != nil { + return fmt.Errorf("ocm-controller requires ocm-registry-tls-certs in ocm-system namespace to exist: %w", err) + } + + if err := o.checkFluxExists(ctx, cfg, rcg); err != nil { + return err + } + + return nil +} + +func (o *Command) checkCertificateSecretExists(ctx context.Context, cfg *rest.Config, rcg *genericclioptions.ConfigFlags) error { + restMapper, err := rcg.ToRESTMapper() + if err != nil { + return err + } + + kubeClient, err := client.New(cfg, client.Options{Mapper: restMapper, Scheme: newScheme()}) + if err != nil { + return err + } + + s := &corev1.Secret{} + return kubeClient.Get(ctx, types.NamespacedName{ + Name: "ocm-registry-tls-certs", + Namespace: "ocm-system", + }, s) +} + +func (o *Command) checkFluxExists(ctx context.Context, cfg *rest.Config, rcg *genericclioptions.ConfigFlags) error { + restMapper, err := rcg.ToRESTMapper() + if err != nil { + return err + } + + kubeClient, err := client.New(cfg, client.Options{Mapper: restMapper, Scheme: newScheme()}) + if err != nil { + return err + } + + s := &corev1.Namespace{} + return kubeClient.Get(ctx, types.NamespacedName{ + Name: "flux-system", + }, s) +} diff --git a/cmds/ocm/commands/controllercmds/install/cmd_test.go b/cmds/ocm/commands/controllercmds/install/cmd_test.go index 14f4ccbb62..13d2c4617f 100644 --- a/cmds/ocm/commands/controllercmds/install/cmd_test.go +++ b/cmds/ocm/commands/controllercmds/install/cmd_test.go @@ -53,20 +53,22 @@ var _ = Describe("Test Environment", func() { It("install latest version", func() { buf := bytes.NewBuffer(nil) - Expect(env.CatchOutput(buf).Execute("controller", "install", "-d", "-u", testServer.URL, "-a", testServer.URL)).To(Succeed()) + Expect(env.CatchOutput(buf).Execute("controller", "install", "-d", "-s", "-u", testServer.URL, "-a", testServer.URL)).To(Succeed()) Expect(buf.String()).To(StringEqualTrimmedWithContext(`► installing ocm-controller with version latest ► got latest version "v0.0.1-test" ✔ successfully fetched install file test: content +✔ ocm-controller successfully installed `)) }) It("install specific version", func() { buf := bytes.NewBuffer(nil) - Expect(env.CatchOutput(buf).Execute("controller", "install", "-d", "-u", testServer.URL, "-a", testServer.URL, "-v", "v0.1.0-test-2")).To(Succeed()) + Expect(env.CatchOutput(buf).Execute("controller", "install", "-d", "-s", "-u", testServer.URL, "-a", testServer.URL, "-v", "v0.1.0-test-2")).To(Succeed()) Expect(buf.String()).To(StringEqualTrimmedWithContext(`► installing ocm-controller with version v0.1.0-test-2 ✔ successfully fetched install file test: content +✔ ocm-controller successfully installed `)) }) }) diff --git a/cmds/ocm/commands/controllercmds/install/install_cert_manager.go b/cmds/ocm/commands/controllercmds/install/install_cert_manager.go new file mode 100644 index 0000000000..8dcde14dd5 --- /dev/null +++ b/cmds/ocm/commands/controllercmds/install/install_cert_manager.go @@ -0,0 +1,73 @@ +package install + +import ( + "context" + "fmt" + "os" + + _ "embed" + + "github.com/fluxcd/pkg/ssa" + "github.com/mandelsoft/filepath/pkg/filepath" + "k8s.io/cli-runtime/pkg/genericclioptions" + + "github.com/open-component-model/ocm/pkg/out" +) + +//go:embed issuer/registry_certificate.yaml +var issuer []byte + +func (o *Command) installPrerequisites(ctx context.Context) error { + out.Outf(o.Context, "► installing cert-manager with version %s\n", o.CertManagerVersion) + + version := o.CertManagerVersion + if err := o.installManifest( + ctx, + o.CertManagerReleaseAPIURL, + o.CertManagerBaseURL, + "cert-manager", + "cert-manager.yaml", + version, + ); err != nil { + return err + } + + out.Outf(o.Context, "✔ cert-manager successfully installed\n") + out.Outf(o.Context, "► creating certificate for internal registry\n") + + if err := o.createRegistryCertificate(); err != nil { + return fmt.Errorf("✗ failed to create registry certificate: %w", err) + } + + return nil +} + +func (o *Command) createRegistryCertificate() error { + temp, err := os.MkdirTemp("", "issuer") + if err != nil { + return fmt.Errorf("failed to create temp folder: %w", err) + } + defer os.RemoveAll(temp) + + path := filepath.Join(temp, "issuer.yaml") + if err := os.WriteFile(path, issuer, 0o600); err != nil { + return fmt.Errorf("failed to write issuer.yaml file at location: %w", err) + } + + kubeconfigArgs := genericclioptions.NewConfigFlags(false) + sm, err := NewResourceManager(kubeconfigArgs) + if err != nil { + return fmt.Errorf("failed to create resource manager: %w", err) + } + + objects, err := readObjects(path) + if err != nil { + return fmt.Errorf("failed to construct objects to apply: %w", err) + } + + if _, err := sm.ApplyAllStaged(context.Background(), objects, ssa.DefaultApplyOptions()); err != nil { + return fmt.Errorf("failed to apply manifests: %w", err) + } + + return nil +} diff --git a/cmds/ocm/commands/controllercmds/install/issuer/registry_certificate.yaml b/cmds/ocm/commands/controllercmds/install/issuer/registry_certificate.yaml new file mode 100644 index 0000000000..a19e12f5b5 --- /dev/null +++ b/cmds/ocm/commands/controllercmds/install/issuer/registry_certificate.yaml @@ -0,0 +1,29 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: ocm-system +--- +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: ocm-issuer +spec: + selfSigned: {} +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: ocm-registry-certificate + namespace: ocm-system +spec: + isCA: true + secretName: ocm-registry-tls-certs + dnsNames: + - registry.ocm-system.svc.cluster.local + privateKey: + algorithm: ECDSA + size: 256 + issuerRef: + name: ocm-issuer + kind: ClusterIssuer + group: cert-manager.io