diff --git a/pkg/controllers/localbuild/argo.go b/pkg/controllers/localbuild/argo.go index 8dbdd9e8..b4347981 100644 --- a/pkg/controllers/localbuild/argo.go +++ b/pkg/controllers/localbuild/argo.go @@ -4,17 +4,11 @@ import ( "context" "embed" "fmt" - "golang.org/x/crypto/bcrypt" - v1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/util/json" - "sigs.k8s.io/controller-runtime/pkg/client" - "time" - "github.com/cnoe-io/idpbuilder/api/v1alpha1" "github.com/cnoe-io/idpbuilder/globals" "github.com/cnoe-io/idpbuilder/pkg/k8s" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" ctrl "sigs.k8s.io/controller-runtime" @@ -24,9 +18,10 @@ import ( var installArgoFS embed.FS const ( - argocdDevModePassword = "developer" - argocdAdminSecretName = "argocd-secret" - argocdNamespace = "argocd" + argocdDevModePassword = "developer" + argocdInitialAdminSecretName = "argocd-initial-admin-secret" + argocdNamespace = "argocd" + argocdIngressURL = "%s://argocd.cnoe.localtest.me:%s" ) func RawArgocdInstallResources(templateData any, config v1alpha1.PackageCustomization, scheme *runtime.Scheme) ([][]byte, error) { @@ -67,76 +62,24 @@ func (r *LocalbuildReconciler) ReconcileArgo(ctx context.Context, req ctrl.Reque if result, err := argocd.Install(ctx, resource, r.Client, r.Scheme, r.Config); err != nil { return result, err } - resource.Status.ArgoCD.Available = true - - // Let's patch the existing argocd admin secret if devmode is enabled to set the default password - if r.Config.DevMode { - // Hash password using bcrypt - password := argocdDevModePassword - hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), 0) - if err != nil { - return ctrl.Result{}, fmt.Errorf("error hashing password: %w", err) - } - - // Getting the argocd-secret - obj := v1.Secret{} - err = r.Client.Get(ctx, client.ObjectKey{Name: argocdAdminSecretName, Namespace: argocdNamespace}, &obj) - if err != nil { - return ctrl.Result{}, fmt.Errorf("getting argocd secret: %w", err) - } - - // Using an unstructured object to avoid managing fields we do not care about. - u := unstructured.Unstructured{} - u.SetName(obj.GetName()) - u.SetNamespace(obj.GetNamespace()) - u.SetGroupVersionKind(obj.GetObjectKind().GroupVersionKind()) - - // Prepare the patch for the Secret's `stringData` field - patchData := map[string]interface{}{ - "stringData": map[string]string{ - "accounts.developer.password": string(hashedPassword), - "accounts.developer.passwordMtime": time.Now().Format(time.RFC3339), - }, - } - // Convert patch data to JSON - patch, err := json.Marshal(patchData) - if err != nil { - return ctrl.Result{}, fmt.Errorf("Error marshalling patch data: %w", err) - } - - // Patching the argocd-secret with the user's hashed password - err = r.Client.Patch(ctx, &u, client.RawPatch(types.MergePatchType, patch)) - if err != nil { - return ctrl.Result{}, fmt.Errorf("error patching the Secret: %w", err) - } - return ctrl.Result{}, nil - - /* - This is not needed as we will not generate a new admin password - adminSecret := v1.Secret{ - TypeMeta: metav1.TypeMeta{ - Kind: "Secret", - APIVersion: "v1", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: argocdInitialAdminSecretName, - Namespace: argocdNamespace, - }, - StringData: map[string]string{ - argocdInitialAdminPasswordKey: argocdDevModePassword, - }, - } - - // Re-creating the initial admin password secret: argocd-initial-admin-secret as used with "idpbuilder get secrets -p argocd" - err = kubeClient.Create(ctx, &adminSecret) - if err != nil { - return ctrl.Result{}, fmt.Errorf("Error creating the initial admin secret: %w", err) - } else { - return ctrl.Result{}, nil - }*/ + resource.Status.ArgoCD.Available = true + return ctrl.Result{}, nil +} +func (r *LocalbuildReconciler) ArgocdInitialAdminSecretObject() corev1.Secret { + return corev1.Secret{ + TypeMeta: metav1.TypeMeta{ + Kind: "Secret", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: argocdInitialAdminSecretName, + Namespace: argocdNamespace, + }, } +} - return ctrl.Result{}, nil +func (r *LocalbuildReconciler) ArgocdBaseUrl(config v1alpha1.BuildCustomizationSpec) string { + return fmt.Sprintf(argocdIngressURL, config.Protocol, config.Port) } diff --git a/pkg/controllers/localbuild/controller.go b/pkg/controllers/localbuild/controller.go index 20a513ed..96c0f9a8 100644 --- a/pkg/controllers/localbuild/controller.go +++ b/pkg/controllers/localbuild/controller.go @@ -1,8 +1,13 @@ package localbuild import ( + "bytes" "context" + "encoding/json" "fmt" + "io" + "k8s.io/apimachinery/pkg/types" + "net/http" "os" "path/filepath" "strings" @@ -39,6 +44,14 @@ const ( argoCDApplicationSetAnnotationKeyRefreshTrue = "true" ) +var ( + status = "failed" +) + +type ArgocdSession struct { + Token string `json:"token"` +} + type LocalbuildReconciler struct { client.Client Scheme *runtime.Scheme @@ -89,6 +102,28 @@ func (r *LocalbuildReconciler) Reconcile(ctx context.Context, req ctrl.Request) } } + if r.Config.DevMode { + logger.Info("DevMode is enabled") + initialPassword, err := r.extractArgocdInitialAdminSecret(ctx) + if err != nil { + // Argocd initial admin secret is not yet available ... + return ctrl.Result{RequeueAfter: defaultRequeueTime}, nil + } + + logger.Info("Initial argocd admin secret found ...") + + // Secret containing the initial argocd password exists + // Lets try to update the password + if initialPassword != "" && status == "failed" { + err, status = r.updateDevPassword(ctx, initialPassword) + if err != nil { + return ctrl.Result{}, err + } else { + logger.Info(fmt.Sprintf("Argocd admin password change %s !", status)) + } + } + } + logger.V(1).Info("done installing core packages. passing control to argocd") _, err = r.ReconcileArgoAppsWithGitea(ctx, req, &localBuild) if err != nil { @@ -581,6 +616,130 @@ func (r *LocalbuildReconciler) requestArgoCDAppSetRefresh(ctx context.Context) e return nil } +func (r *LocalbuildReconciler) extractArgocdInitialAdminSecret(ctx context.Context) (string, error) { + sec := r.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) updateDevPassword(ctx context.Context, adminPassword string) (error, string) { + argocdEndpoint := r.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), "failed" + } + + // 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), "failed" + } + req.Header.Set("Content-Type", "application/json") + + // Create an HTTP client and disable TLS verification + client := &http.Client{} + transport := http.DefaultTransport.(*http.Transport).Clone() + transport.TLSClientConfig.InsecureSkipVerify = true + client.Transport = transport + + // Send the request + resp, err := client.Do(req) + if err != nil { + return fmt.Errorf("Error sending request: %v\n", err), "failed" + } + 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), "failed" + } + + if resp.StatusCode == 200 { + var argocdSession ArgocdSession + + err := json.Unmarshal([]byte(body), &argocdSession) + if err != nil { + fmt.Errorf("Error unmarshalling JSON: %v", err) + } + + payload := map[string]string{ + "name": "admin", + "currentPassword": adminPassword, + "newPassword": argocdDevModePassword, + } + + payloadBytes, err := json.Marshal(payload) + if err != nil { + return fmt.Errorf("Error creating JSON payload: %v\n", err), "failed" + } + + req, err := http.NewRequest("PUT", argocdEndpoint+"/account/password", bytes.NewBuffer(payloadBytes)) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", argocdSession.Token)) + req.Header.Set("Content-Type", "application/json") + + resp, err := client.Do(req) + if err != nil { + return fmt.Errorf("Error sending request: %v\n", err), "failed" + } + defer resp.Body.Close() + + // Lets checking the new admin password + payload = map[string]string{ + "username": "admin", + "password": argocdDevModePassword, + } + payloadBytes, err = json.Marshal(payload) + if err != nil { + return fmt.Errorf("Error creating JSON payload: %v\n", err), "failed" + } + + req, err = http.NewRequest("POST", argocdEndpoint+"/session", bytes.NewBuffer(payloadBytes)) + if err != nil { + return fmt.Errorf("Error creating HTTP request: %v\n", err), "failed" + } + req.Header.Set("Content-Type", "application/json") + + // Create an HTTP client and disable TLS verification + client := &http.Client{} + transport := http.DefaultTransport.(*http.Transport).Clone() + transport.TLSClientConfig.InsecureSkipVerify = true + client.Transport = transport + + // Send the request + resp, err = client.Do(req) + if err != nil { + return fmt.Errorf("Error sending request: %v\n", err), "failed" + } + defer resp.Body.Close() + + if resp.StatusCode == 200 { + // Password verification succeeded ! + return nil, "succeeded" + } else { + return fmt.Errorf("### New password verification failed: %s", body), "failed" + } + + } else { + return fmt.Errorf("HTTP Error: %d", resp.StatusCode), "failed" + } + return nil, "failed" +} + func (r *LocalbuildReconciler) applyArgoCDAnnotation(ctx context.Context, obj client.Object, argoCDType, annotationKey, annotationValue string) error { annotations := obj.GetAnnotations() if annotations != nil {