Skip to content

Commit

Permalink
Refactor the code to update the argocd password post reconciliation. c…
Browse files Browse the repository at this point in the history
…noe-io#441

Signed-off-by: cmoulliard <[email protected]>
  • Loading branch information
cmoulliard committed Dec 9, 2024
1 parent 3c3511d commit bf8e2cc
Show file tree
Hide file tree
Showing 2 changed files with 181 additions and 79 deletions.
101 changes: 22 additions & 79 deletions pkg/controllers/localbuild/argo.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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) {
Expand Down Expand Up @@ -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)
}
159 changes: 159 additions & 0 deletions pkg/controllers/localbuild/controller.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
package localbuild

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"k8s.io/apimachinery/pkg/types"
"net/http"
"os"
"path/filepath"
"strings"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down

0 comments on commit bf8e2cc

Please sign in to comment.