diff --git a/api/v1alpha1/localbuild_types.go b/api/v1alpha1/localbuild_types.go index 62d53ba6..1f9035f2 100644 --- a/api/v1alpha1/localbuild_types.go +++ b/api/v1alpha1/localbuild_types.go @@ -53,6 +53,7 @@ type BuildCustomizationSpec struct { Port string `json:"port,omitempty"` UsePathRouting bool `json:"usePathRouting,omitempty"` SelfSignedCert string `json:"selfSignedCert,omitempty"` + StaticPassword bool `json:"staticPassword,omitempty"` } type LocalbuildSpec struct { diff --git a/hack/argo-cd/argocd-cm.yaml b/hack/argo-cd/argocd-cm.yaml index 8b191064..1ccf0702 100644 --- a/hack/argo-cd/argocd-cm.yaml +++ b/hack/argo-cd/argocd-cm.yaml @@ -4,6 +4,7 @@ metadata: name: argocd-cm data: application.resourceTrackingMethod: annotation + accounts.developer: apiKey, login timeout.reconciliation: 60s resource.exclusions: | - kinds: diff --git a/hack/argo-cd/argocd-rbac-dev.yaml b/hack/argo-cd/argocd-rbac-dev.yaml new file mode 100644 index 00000000..37e3c105 --- /dev/null +++ b/hack/argo-cd/argocd-rbac-dev.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + labels: + app.kubernetes.io/name: argocd-rbac-cm + app.kubernetes.io/part-of: argocd + name: argocd-rbac-cm + namespace: argocd +data: + policy.csv: | + p, role:developer, applications, *, *, allow + g, developer, role:developer \ No newline at end of file diff --git a/hack/argo-cd/kustomization.yaml b/hack/argo-cd/kustomization.yaml index 25872b10..8f4672f3 100644 --- a/hack/argo-cd/kustomization.yaml +++ b/hack/argo-cd/kustomization.yaml @@ -2,6 +2,7 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - https://raw.githubusercontent.com/argoproj/argo-cd/v2.10.7/manifests/install.yaml + - argocd-rbac-dev.yaml patches: - path: dex-server.yaml diff --git a/pkg/build/build.go b/pkg/build/build.go index 0da1cb4f..33555efb 100644 --- a/pkg/build/build.go +++ b/pkg/build/build.go @@ -278,5 +278,6 @@ func isBuildCustomizationSpecEqual(s1, s2 v1alpha1.BuildCustomizationSpec) bool s1.IngressHost == s2.IngressHost && s1.Port == s2.Port && s1.UsePathRouting == s2.UsePathRouting && - s1.SelfSignedCert == s2.SelfSignedCert + s1.SelfSignedCert == s2.SelfSignedCert && + s1.StaticPassword == s2.StaticPassword } diff --git a/pkg/cmd/create/root.go b/pkg/cmd/create/root.go index 32212ccd..08f05cef 100644 --- a/pkg/cmd/create/root.go +++ b/pkg/cmd/create/root.go @@ -20,6 +20,7 @@ import ( const ( recreateClusterUsage = "Delete cluster first if it already exists." buildNameUsage = "Name for build (Prefix for kind cluster name, pod names, etc)." + devPasswordUsage = "Set the password \"developer\" for the admin user of the applications: argocd & gitea." kubeVersionUsage = "Version of the kind kubernetes cluster to create." extraPortsMappingUsage = "List of extra ports to expose on the docker container and kubernetes cluster as nodePort " + "(e.g. \"22:32222,9090:39090,etc\")." @@ -40,6 +41,7 @@ var ( // Flags recreateCluster bool buildName string + devPassword bool kubeVersion string extraPortsMapping string kindConfigPath string @@ -67,6 +69,7 @@ func init() { CreateCmd.PersistentFlags().StringVar(&buildName, "build-name", "localdev", buildNameUsage) CreateCmd.PersistentFlags().MarkDeprecated("build-name", "use --name instead.") CreateCmd.PersistentFlags().StringVar(&buildName, "name", "localdev", buildNameUsage) + CreateCmd.PersistentFlags().BoolVar(&devPassword, "dev-password", false, devPasswordUsage) CreateCmd.PersistentFlags().StringVar(&kubeVersion, "kube-version", "v1.30.3", kubeVersionUsage) CreateCmd.PersistentFlags().StringVar(&extraPortsMapping, "extra-ports", "", extraPortsMappingUsage) CreateCmd.PersistentFlags().StringVar(&kindConfigPath, "kind-config", "", kindConfigPathUsage) @@ -143,6 +146,7 @@ func create(cmd *cobra.Command, args []string) error { IngressHost: ingressHost, Port: port, UsePathRouting: pathRouting, + StaticPassword: devPassword, }, CustomPackageDirs: absDirPaths, diff --git a/pkg/cmd/get/secrets.go b/pkg/cmd/get/secrets.go index 5cad39c3..6cec892b 100644 --- a/pkg/cmd/get/secrets.go +++ b/pkg/cmd/get/secrets.go @@ -5,6 +5,7 @@ import ( "embed" "encoding/json" "fmt" + "github.com/cnoe-io/idpbuilder/pkg/util" "io" "os" "path/filepath" @@ -206,6 +207,7 @@ func secretToTemplateData(s v1.Secret) TemplateData { for k, v := range s.Data { data.Data[k] = string(v) } + return data } @@ -223,13 +225,8 @@ func getSecretsByCNOELabel(ctx context.Context, kubeClient client.Client, l labe return secrets, kubeClient.List(ctx, &secrets, &opts) } -func getSecretByName(ctx context.Context, kubeClient client.Client, ns, name string) (v1.Secret, error) { - s := v1.Secret{} - return s, kubeClient.Get(ctx, client.ObjectKey{Name: name, Namespace: ns}, &s) -} - func getCorePackageSecret(ctx context.Context, kubeClient client.Client, ns, name string) (v1.Secret, error) { - s, err := getSecretByName(ctx, kubeClient, ns, name) + s, err := util.GetSecretByName(ctx, kubeClient, ns, name) if err != nil { return v1.Secret{}, err } diff --git a/pkg/controllers/gitrepository/controller.go b/pkg/controllers/gitrepository/controller.go index 6a303494..93baedef 100644 --- a/pkg/controllers/gitrepository/controller.go +++ b/pkg/controllers/gitrepository/controller.go @@ -143,6 +143,10 @@ func (r *RepositoryReconciler) reconcileGitRepo(ctx context.Context, repo *v1alp return ctrl.Result{}, fmt.Errorf("getting git provider credentials: %w", err) } + if r.Config.StaticPassword { + creds.password = util.StaticPassword + } + err = provider.setProviderCredentials(ctx, repo, creds) if err != nil { return ctrl.Result{}, fmt.Errorf("setting git provider credentials: %w", err) diff --git a/pkg/controllers/gitrepository/gitea.go b/pkg/controllers/gitrepository/gitea.go index de82970f..a19a433f 100644 --- a/pkg/controllers/gitrepository/gitea.go +++ b/pkg/controllers/gitrepository/gitea.go @@ -69,6 +69,7 @@ func (g *giteaProvider) getProviderCredentials(ctx context.Context, repo *v1alph if !ok { return gitProviderCredentials{}, fmt.Errorf("%s key not found in secret %s in %s ns", giteaAdminPasswordKey, repo.Spec.SecretRef.Name, repo.Spec.SecretRef.Namespace) } + return gitProviderCredentials{ username: string(username), password: string(password), diff --git a/pkg/controllers/localbuild/argo.go b/pkg/controllers/localbuild/argo.go index bf21aa1d..d55073c4 100644 --- a/pkg/controllers/localbuild/argo.go +++ b/pkg/controllers/localbuild/argo.go @@ -3,7 +3,6 @@ package localbuild import ( "context" "embed" - "github.com/cnoe-io/idpbuilder/api/v1alpha1" "github.com/cnoe-io/idpbuilder/globals" "github.com/cnoe-io/idpbuilder/pkg/k8s" diff --git a/pkg/controllers/localbuild/argo_test.go b/pkg/controllers/localbuild/argo_test.go index e35ec4c6..abad74b6 100644 --- a/pkg/controllers/localbuild/argo_test.go +++ b/pkg/controllers/localbuild/argo_test.go @@ -80,8 +80,8 @@ func TestGetK8sInstallResources(t *testing.T) { t.Fatalf("GetK8sInstallResources() error: %v", err) } - if len(objs) != 58 { - t.Fatalf("Expected 58 Argo Install Resources, got: %d", len(objs)) + if len(objs) != 59 { + t.Fatalf("Expected 59 Argo Install Resources, got: %d", len(objs)) } } diff --git a/pkg/controllers/localbuild/controller.go b/pkg/controllers/localbuild/controller.go index 20a513ed..11327286 100644 --- a/pkg/controllers/localbuild/controller.go +++ b/pkg/controllers/localbuild/controller.go @@ -1,8 +1,14 @@ package localbuild import ( + "bytes" + "code.gitea.io/sdk/gitea" "context" + "encoding/json" "fmt" + "io" + "k8s.io/apimachinery/pkg/types" + "net/http" "os" "path/filepath" "strings" @@ -39,6 +45,10 @@ const ( argoCDApplicationSetAnnotationKeyRefreshTrue = "true" ) +type ArgocdSession struct { + Token string `json:"token"` +} + type LocalbuildReconciler struct { client.Client Scheme *runtime.Scheme @@ -89,6 +99,48 @@ func (r *LocalbuildReconciler) Reconcile(ctx context.Context, req ctrl.Request) } } + if r.Config.StaticPassword { + logger.V(1).Info("static password is enabled") + + // Check if the Argocd Initial admin secret exists + argocdInitialAdminPassword, err := r.extractArgocdInitialAdminSecret(ctx) + if err != nil { + // Argocd initial admin secret is not yet available ... + return ctrl.Result{RequeueAfter: defaultRequeueTime}, nil + } + + logger.V(1).Info("Initial argocd admin secret found ...") + + // Secret containing the initial argocd password exists + // Lets try to update the password + if argocdInitialAdminPassword != "" { + err = r.updateArgocdPassword(ctx, argocdInitialAdminPassword) + if err != nil { + return ctrl.Result{}, err + } else { + logger.V(1).Info(fmt.Sprintf("Argocd admin password change succeeded !")) + } + } + + // Check if the Gitea credentials secret exists + giteaAdminPassword, err := r.extractGiteaAdminSecret(ctx) + if err != nil { + // Gitea admin secret is not yet available ... + return ctrl.Result{RequeueAfter: defaultRequeueTime}, nil + } + logger.V(1).Info("Gitea admin secret found ...") + // Secret containing the gitea password exists + // Lets try to update the password + if giteaAdminPassword != "" { + err = r.updateGiteaPassword(ctx, giteaAdminPassword) + if err != nil { + return ctrl.Result{}, err + } else { + logger.V(1).Info(fmt.Sprintf("Gitea admin password change succeeded !")) + } + } + } + logger.V(1).Info("done installing core packages. passing control to argocd") _, err = r.ReconcileArgoAppsWithGitea(ctx, req, &localBuild) if err != nil { @@ -581,6 +633,166 @@ func (r *LocalbuildReconciler) requestArgoCDAppSetRefresh(ctx context.Context) e return nil } +func (r *LocalbuildReconciler) extractArgocdInitialAdminSecret(ctx context.Context) (string, error) { + sec := util.ArgocdInitialAdminSecretObject() + err := r.Client.Get(ctx, types.NamespacedName{ + Namespace: sec.GetNamespace(), + Name: sec.GetName(), + }, &sec) + + if err != nil { + if k8serrors.IsNotFound(err) { + return "", fmt.Errorf("initial admin secret not found") + } + } + return string(sec.Data["password"]), nil +} + +func (r *LocalbuildReconciler) extractGiteaAdminSecret(ctx context.Context) (string, error) { + sec := util.GiteaAdminSecretObject() + err := r.Client.Get(ctx, types.NamespacedName{ + Namespace: sec.GetNamespace(), + Name: sec.GetName(), + }, &sec) + + if err != nil { + if k8serrors.IsNotFound(err) { + return "", fmt.Errorf("gitea admin secret not found") + } + } + return string(sec.Data["password"]), nil +} + +func (r *LocalbuildReconciler) updateGiteaPassword(ctx context.Context, adminPassword string) error { + client, err := gitea.NewClient(util.GiteaBaseUrl(r.Config), gitea.SetHTTPClient(util.GetHttpClient()), + gitea.SetBasicAuth("giteaAdmin", adminPassword), gitea.SetContext(ctx), + ) + if err != nil { + return fmt.Errorf("cannot create gitea client: %w", err) + } + + opts := gitea.EditUserOption{ + LoginName: "giteaAdmin", + Password: util.StaticPassword, + } + + resp, err := client.AdminEditUser("giteaAdmin", opts) + if err != nil { + return fmt.Errorf("cannot update gitea admin user. status: %d error : %w", resp.StatusCode, err) + } + + err = util.PatchPasswordSecret(ctx, r.Client, r.Config, util.GiteaNamespace, util.GiteaAdminSecret, util.GiteaAdminName, util.StaticPassword) + if err != nil { + return fmt.Errorf("patching the gitea credentials failed : %w", err) + } + return nil +} + +func (r *LocalbuildReconciler) updateArgocdPassword(ctx context.Context, adminPassword string) error { + argocdEndpoint := util.ArgocdBaseUrl(r.Config) + "/api/v1" + + payload := map[string]string{ + "username": "admin", + "password": adminPassword, + } + payloadBytes, err := json.Marshal(payload) + if err != nil { + return fmt.Errorf("Error creating JSON payload: %v\n", err) + } + + // Create an HTTP POST request to get the Session token + req, err := http.NewRequest("POST", argocdEndpoint+"/session", bytes.NewBuffer(payloadBytes)) + if err != nil { + return fmt.Errorf("Error creating HTTP request: %v\n", err) + } + req.Header.Set("Content-Type", "application/json") + + // Create an HTTP c and disable TLS verification + c := util.GetHttpClient() + + // Send the request + resp, err := c.Do(req) + if err != nil { + return fmt.Errorf("Error sending request: %v\n", err) + } + defer resp.Body.Close() + + // Read the response body + body, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("Error reading response body: %v\n", err) + } + + // We got a session Token, so we can update the Argocd admin password + if resp.StatusCode == 200 { + var argocdSession ArgocdSession + + err := json.Unmarshal([]byte(body), &argocdSession) + if err != nil { + return fmt.Errorf("Error unmarshalling JSON: %v", err) + } + + payload := map[string]string{ + "name": "admin", + "currentPassword": adminPassword, + "newPassword": util.StaticPassword, + } + + payloadBytes, err := json.Marshal(payload) + if err != nil { + return fmt.Errorf("Error creating JSON payload: %v\n", err) + } + + req, err := http.NewRequest("PUT", argocdEndpoint+"/account/password", bytes.NewBuffer(payloadBytes)) + if req != nil { + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", argocdSession.Token)) + req.Header.Set("Content-Type", "application/json") + } + + resp, err := c.Do(req) + if err != nil { + return fmt.Errorf("Error sending request: %v\n", err) + } + defer resp.Body.Close() + + // Lets checking the new admin password + payload = map[string]string{ + "username": "admin", + "password": util.StaticPassword, + } + payloadBytes, err = json.Marshal(payload) + if err != nil { + return fmt.Errorf("Error creating JSON payload: %v\n", err) + } + + // Define the request able to verify if the username and password changed works + req, err = http.NewRequest("POST", argocdEndpoint+"/session", bytes.NewBuffer(payloadBytes)) + if err != nil { + return fmt.Errorf("Error creating HTTP request: %v\n", err) + } + req.Header.Set("Content-Type", "application/json") + + // Send the request + resp, err = c.Do(req) + if err != nil { + return fmt.Errorf("Error sending request: %v\n", err) + } + defer resp.Body.Close() + + // Password verification succeeded ! + if resp.StatusCode == 200 { + // Let's patch the existing secret now + err = util.PatchPasswordSecret(ctx, r.Client, r.Config, util.ArgocdNamespace, util.ArgocdInitialAdminSecretName, util.ArgocdAdminName, util.StaticPassword) + if err != nil { + return fmt.Errorf("patching the argocd initial secret failed : %w", err) + } + return nil + } + } + // No session token has been received and by consequence the admin password has not been changed + return nil +} + func (r *LocalbuildReconciler) applyArgoCDAnnotation(ctx context.Context, obj client.Object, argoCDType, annotationKey, annotationValue string) error { annotations := obj.GetAnnotations() if annotations != nil { diff --git a/pkg/controllers/localbuild/gitea.go b/pkg/controllers/localbuild/gitea.go index 79f844c8..83c75f30 100644 --- a/pkg/controllers/localbuild/gitea.go +++ b/pkg/controllers/localbuild/gitea.go @@ -7,13 +7,11 @@ import ( "fmt" "net/http" - "code.gitea.io/sdk/gitea" "github.com/cnoe-io/idpbuilder/api/v1alpha1" "github.com/cnoe-io/idpbuilder/pkg/k8s" "github.com/cnoe-io/idpbuilder/pkg/util" corev1 "k8s.io/api/core/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" @@ -23,19 +21,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log" ) -const ( - // hardcoded values from what we have in the yaml installation file. - giteaNamespace = "gitea" - giteaAdminSecret = "gitea-credential" - giteaAdminTokenName = "admin" - giteaAdminTokenFieldName = "token" - // this is the URL accessible outside cluster. resolves to localhost - giteaIngressURL = "%s://gitea.cnoe.localtest.me:%s" - // this is the URL accessible within cluster for ArgoCD to fetch resources. - // resolves to cluster ip - giteaSvcURL = "%s://%s%s:%s%s" -) - //go:embed resources/gitea/k8s/* var installGiteaFS embed.FS @@ -43,30 +28,13 @@ func RawGiteaInstallResources(templateData any, config v1alpha1.PackageCustomiza return k8s.BuildCustomizedManifests(config.FilePath, "resources/gitea/k8s", installGiteaFS, scheme, templateData) } -func giteaAdminSecretObject() corev1.Secret { - return corev1.Secret{ - TypeMeta: metav1.TypeMeta{ - Kind: "Secret", - APIVersion: "v1", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: giteaAdminSecret, - Namespace: giteaNamespace, - }, - } -} - -func newGiteaAdminSecret() (corev1.Secret, error) { - pass, err := util.GeneratePassword() - if err != nil { - return corev1.Secret{}, err - } - obj := giteaAdminSecretObject() +func (r *LocalbuildReconciler) newGiteaAdminSecret(password string) corev1.Secret { + obj := util.GiteaAdminSecretObject() obj.StringData = map[string]string{ "username": v1alpha1.GiteaAdminUserName, - "password": pass, + "password": password, } - return obj, nil + return obj } func (r *LocalbuildReconciler) ReconcileGitea(ctx context.Context, req ctrl.Request, resource *v1alpha1.Localbuild) (ctrl.Result, error) { @@ -75,7 +43,7 @@ func (r *LocalbuildReconciler) ReconcileGitea(ctx context.Context, req ctrl.Requ name: "Gitea", resourcePath: "resources/gitea/k8s", resourceFS: installGiteaFS, - namespace: giteaNamespace, + namespace: util.GiteaNamespace, monitoredResources: map[string]schema.GroupVersionKind{ "my-gitea": { Group: "apps", @@ -85,7 +53,7 @@ func (r *LocalbuildReconciler) ReconcileGitea(ctx context.Context, req ctrl.Requ }, } - sec := giteaAdminSecretObject() + sec := util.GiteaAdminSecretObject() err := r.Client.Get(ctx, types.NamespacedName{ Namespace: sec.GetNamespace(), Name: sec.GetName(), @@ -93,7 +61,12 @@ func (r *LocalbuildReconciler) ReconcileGitea(ctx context.Context, req ctrl.Requ if err != nil { if k8serrors.IsNotFound(err) { - giteaCreds, err := newGiteaAdminSecret() + genPassword, err := util.GeneratePassword() + if err != nil { + return ctrl.Result{}, fmt.Errorf("generating gitea password: %w", err) + } + + giteaCreds := r.newGiteaAdminSecret(genPassword) if err != nil { return ctrl.Result{}, fmt.Errorf("generating gitea admin secret: %w", err) } @@ -113,7 +86,7 @@ func (r *LocalbuildReconciler) ReconcileGitea(ctx context.Context, req ctrl.Requ return result, err } - baseUrl := giteaBaseUrl(r.Config) + baseUrl := util.GiteaBaseUrl(r.Config) // need this to ensure gitrepository controller can reach the api endpoint. logger.V(1).Info("checking gitea api endpoint", "url", baseUrl) c := util.GetHttpClient() @@ -136,21 +109,21 @@ func (r *LocalbuildReconciler) ReconcileGitea(ctx context.Context, req ctrl.Requ resource.Status.Gitea.ExternalURL = baseUrl resource.Status.Gitea.InternalURL = giteaInternalBaseUrl(r.Config) - resource.Status.Gitea.AdminUserSecretName = giteaAdminSecret - resource.Status.Gitea.AdminUserSecretNamespace = giteaNamespace + resource.Status.Gitea.AdminUserSecretName = util.GiteaAdminSecret + resource.Status.Gitea.AdminUserSecretNamespace = util.GiteaNamespace resource.Status.Gitea.Available = true return ctrl.Result{}, nil } func (r *LocalbuildReconciler) setGiteaToken(ctx context.Context, secret corev1.Secret, baseUrl string) error { - _, ok := secret.Data[giteaAdminTokenFieldName] + _, ok := secret.Data[util.GiteaAdminTokenFieldName] if ok { return nil } u := unstructured.Unstructured{} - u.SetName(giteaAdminSecret) - u.SetNamespace(giteaNamespace) + u.SetName(util.GiteaAdminSecret) + u.SetNamespace(util.GiteaNamespace) u.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind("Secret")) user, ok := secret.Data["username"] @@ -163,13 +136,13 @@ func (r *LocalbuildReconciler) setGiteaToken(ctx context.Context, secret corev1. return fmt.Errorf("password field not found in gitea secret") } - t, err := getGiteaToken(ctx, baseUrl, string(user), string(pass)) + t, err := util.GetGiteaToken(ctx, baseUrl, string(user), string(pass)) if err != nil { return fmt.Errorf("getting gitea token: %w", err) } token := base64.StdEncoding.EncodeToString([]byte(t)) - err = unstructured.SetNestedField(u.Object, token, "data", giteaAdminTokenFieldName) + err = unstructured.SetNestedField(u.Object, token, "data", util.GiteaAdminTokenFieldName) if err != nil { return fmt.Errorf("setting gitea token field: %w", err) } @@ -177,49 +150,10 @@ func (r *LocalbuildReconciler) setGiteaToken(ctx context.Context, secret corev1. return r.Client.Patch(ctx, &u, client.Apply, client.ForceOwnership, client.FieldOwner(v1alpha1.FieldManager)) } -func getGiteaToken(ctx context.Context, baseUrl, username, password string) (string, error) { - giteaClient, err := gitea.NewClient(baseUrl, gitea.SetHTTPClient(util.GetHttpClient()), - gitea.SetBasicAuth(username, password), gitea.SetContext(ctx), - ) - if err != nil { - return "", fmt.Errorf("creating gitea client: %w", err) - } - tokens, resp, err := giteaClient.ListAccessTokens(gitea.ListAccessTokensOptions{}) - if err != nil { - return "", fmt.Errorf("listing gitea access tokens. status: %s error : %w", resp.Status, err) - } - - for i := range tokens { - if tokens[i].Name == giteaAdminTokenName { - resp, err := giteaClient.DeleteAccessToken(tokens[i].ID) - if err != nil { - return "", fmt.Errorf("deleting gitea access tokens. status: %s error : %w", resp.Status, err) - } - break - } - } - - token, resp, err := giteaClient.CreateAccessToken(gitea.CreateAccessTokenOption{ - Name: giteaAdminTokenName, - Scopes: []gitea.AccessTokenScope{ - gitea.AccessTokenScopeAll, - }, - }) - if err != nil { - return "", fmt.Errorf("deleting gitea access tokens. status: %s error : %w", resp.Status, err) - } - - return token.Token, nil -} - -func giteaBaseUrl(config v1alpha1.BuildCustomizationSpec) string { - return fmt.Sprintf(giteaIngressURL, config.Protocol, config.Port) -} - // gitea URL reachable within the cluster with proper coredns config. Mainly for argocd func giteaInternalBaseUrl(config v1alpha1.BuildCustomizationSpec) string { if config.UsePathRouting { - return fmt.Sprintf(giteaSvcURL, config.Protocol, "", config.Host, config.Port, "/gitea") + return fmt.Sprintf(util.GiteaSvcURL, config.Protocol, "", config.Host, config.Port, "/gitea") } - return fmt.Sprintf(giteaSvcURL, config.Protocol, "gitea.", config.Host, config.Port, "") + return fmt.Sprintf(util.GiteaSvcURL, config.Protocol, "gitea.", config.Host, config.Port, "") } diff --git a/pkg/controllers/localbuild/gitea_test.go b/pkg/controllers/localbuild/gitea_test.go index b4d0c761..bfda00fa 100644 --- a/pkg/controllers/localbuild/gitea_test.go +++ b/pkg/controllers/localbuild/gitea_test.go @@ -2,6 +2,7 @@ package localbuild import ( "context" + "github.com/cnoe-io/idpbuilder/pkg/util" "net/http" "net/http/httptest" "testing" @@ -33,6 +34,6 @@ func TestGetGiteaToken(t *testing.T) { })) defer ts.Close() ctx := context.Background() - _, err := getGiteaToken(ctx, ts.URL, "", "") + _, err := util.GetGiteaToken(ctx, ts.URL, "", "") require.Error(t, err) } diff --git a/pkg/controllers/localbuild/resources/argo/install.yaml b/pkg/controllers/localbuild/resources/argo/install.yaml index 28dae6ce..6eea371d 100644 --- a/pkg/controllers/localbuild/resources/argo/install.yaml +++ b/pkg/controllers/localbuild/resources/argo/install.yaml @@ -21081,6 +21081,20 @@ subjects: --- apiVersion: v1 data: + policy.csv: |- + p, role:developer, applications, *, *, allow + g, developer, role:developer +kind: ConfigMap +metadata: + labels: + app.kubernetes.io/name: argocd-rbac-cm + app.kubernetes.io/part-of: argocd + name: argocd-rbac-cm + namespace: argocd +--- +apiVersion: v1 +data: + accounts.developer: apiKey, login application.resourceTrackingMethod: annotation resource.exclusions: | - kinds: diff --git a/pkg/controllers/resources/idpbuilder.cnoe.io_localbuilds.yaml b/pkg/controllers/resources/idpbuilder.cnoe.io_localbuilds.yaml index ab98210f..98c37c03 100644 --- a/pkg/controllers/resources/idpbuilder.cnoe.io_localbuilds.yaml +++ b/pkg/controllers/resources/idpbuilder.cnoe.io_localbuilds.yaml @@ -51,6 +51,8 @@ spec: type: string selfSignedCert: type: string + staticPasswords: + type: boolean usePathRouting: type: boolean type: object diff --git a/pkg/k8s/util.go b/pkg/k8s/util.go index df40195a..68b5366d 100644 --- a/pkg/k8s/util.go +++ b/pkg/k8s/util.go @@ -2,10 +2,9 @@ package k8s import ( "embed" - "os" - "github.com/cnoe-io/idpbuilder/pkg/util" "k8s.io/apimachinery/pkg/runtime" + "os" "sigs.k8s.io/controller-runtime/pkg/client" ) diff --git a/pkg/util/argocd.go b/pkg/util/argocd.go new file mode 100644 index 00000000..91700d8f --- /dev/null +++ b/pkg/util/argocd.go @@ -0,0 +1,32 @@ +package util + +import ( + "fmt" + "github.com/cnoe-io/idpbuilder/api/v1alpha1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + ArgocdInitialAdminSecretName = "argocd-initial-admin-secret" + ArgocdAdminName = "admin" + ArgocdNamespace = "argocd" + ArgocdIngressURL = "%s://argocd.cnoe.localtest.me:%s" +) + +func ArgocdBaseUrl(config v1alpha1.BuildCustomizationSpec) string { + return fmt.Sprintf(ArgocdIngressURL, config.Protocol, config.Port) +} + +func ArgocdInitialAdminSecretObject() corev1.Secret { + return corev1.Secret{ + TypeMeta: metav1.TypeMeta{ + Kind: "Secret", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: ArgocdInitialAdminSecretName, + Namespace: ArgocdNamespace, + }, + } +} diff --git a/pkg/util/gitea.go b/pkg/util/gitea.go new file mode 100644 index 00000000..43dda037 --- /dev/null +++ b/pkg/util/gitea.go @@ -0,0 +1,112 @@ +package util + +import ( + "code.gitea.io/sdk/gitea" + "context" + "encoding/base64" + "fmt" + "github.com/cnoe-io/idpbuilder/api/v1alpha1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "sigs.k8s.io/controller-runtime/pkg/client" + "strings" +) + +const ( + // hardcoded values from what we have in the yaml installation file. + GiteaNamespace = "gitea" + GiteaAdminSecret = "gitea-credential" + GiteaAdminName = "giteaAdmin" + GiteaAdminTokenName = "admin" + GiteaAdminTokenFieldName = "token" + // this is the URL accessible outside cluster. resolves to localhost + GiteaIngressURL = "%s://gitea.cnoe.localtest.me:%s" + // this is the URL accessible within cluster for ArgoCD to fetch resources. + // resolves to cluster ip + GiteaSvcURL = "%s://%s%s:%s%s" +) + +func GiteaAdminSecretObject() corev1.Secret { + return corev1.Secret{ + TypeMeta: metav1.TypeMeta{ + Kind: "Secret", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: GiteaAdminSecret, + Namespace: GiteaNamespace, + }, + } +} + +func PatchPasswordSecret(ctx context.Context, kubeClient client.Client, config v1alpha1.BuildCustomizationSpec, ns string, secretName string, username string, pass string) error { + sec, err := GetSecretByName(ctx, kubeClient, ns, secretName) + if err != nil { + return fmt.Errorf("getting secret to patch fails: %w", err) + } + u := unstructured.Unstructured{} + u.SetName(sec.GetName()) + u.SetNamespace(sec.GetNamespace()) + u.SetGroupVersionKind(sec.GetObjectKind().GroupVersionKind()) + + err = unstructured.SetNestedField(u.Object, base64.StdEncoding.EncodeToString([]byte(pass)), "data", "password") + if err != nil { + return fmt.Errorf("setting password field: %w", err) + } + + if strings.Contains(secretName, "gitea") { + // We should recreate a token as user/password changed + t, err := GetGiteaToken(ctx, GiteaBaseUrl(config), string(username), string(pass)) + if err != nil { + return fmt.Errorf("getting gitea token: %w", err) + } + + token := base64.StdEncoding.EncodeToString([]byte(t)) + err = unstructured.SetNestedField(u.Object, token, "data", GiteaAdminTokenFieldName) + if err != nil { + return fmt.Errorf("setting gitea token field: %w", err) + } + } + + return kubeClient.Patch(ctx, &u, client.Apply, client.ForceOwnership, client.FieldOwner(v1alpha1.FieldManager)) +} + +func GetGiteaToken(ctx context.Context, baseUrl, username, password string) (string, error) { + giteaClient, err := gitea.NewClient(baseUrl, gitea.SetHTTPClient(GetHttpClient()), + gitea.SetBasicAuth(username, password), gitea.SetContext(ctx), + ) + if err != nil { + return "", fmt.Errorf("creating gitea client: %w", err) + } + tokens, resp, err := giteaClient.ListAccessTokens(gitea.ListAccessTokensOptions{}) + if err != nil { + return "", fmt.Errorf("listing gitea access tokens. status: %s error : %w", resp.Status, err) + } + + for i := range tokens { + if tokens[i].Name == GiteaAdminTokenName { + resp, err := giteaClient.DeleteAccessToken(tokens[i].ID) + if err != nil { + return "", fmt.Errorf("deleting gitea access tokens. status: %s error : %w", resp.Status, err) + } + break + } + } + + token, resp, err := giteaClient.CreateAccessToken(gitea.CreateAccessTokenOption{ + Name: GiteaAdminTokenName, + Scopes: []gitea.AccessTokenScope{ + gitea.AccessTokenScopeAll, + }, + }) + if err != nil { + return "", fmt.Errorf("deleting gitea access tokens. status: %s error : %w", resp.Status, err) + } + + return token.Token, nil +} + +func GiteaBaseUrl(config v1alpha1.BuildCustomizationSpec) string { + return fmt.Sprintf(GiteaIngressURL, config.Protocol, config.Port) +} diff --git a/pkg/util/secret.go b/pkg/util/secret.go new file mode 100644 index 00000000..207d0b09 --- /dev/null +++ b/pkg/util/secret.go @@ -0,0 +1,12 @@ +package util + +import ( + "context" + v1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func GetSecretByName(ctx context.Context, kubeClient client.Client, ns, name string) (v1.Secret, error) { + s := v1.Secret{} + return s, kubeClient.Get(ctx, client.ObjectKey{Name: name, Namespace: ns}, &s) +} diff --git a/pkg/util/util.go b/pkg/util/util.go index 86f4fad3..99b15cb0 100644 --- a/pkg/util/util.go +++ b/pkg/util/util.go @@ -28,6 +28,7 @@ const ( passwordLength = 40 numSpecialChars = 3 numDigits = 3 + StaticPassword = "developer" ) func GetCLIStartTimeAnnotationValue(annotations map[string]string) (string, error) {