diff --git a/k8-operator/api/v1alpha1/infisicalsecret_types.go b/k8-operator/api/v1alpha1/infisicalsecret_types.go index 8ad859e13..d7a7c2eab 100644 --- a/k8-operator/api/v1alpha1/infisicalsecret_types.go +++ b/k8-operator/api/v1alpha1/infisicalsecret_types.go @@ -16,8 +16,8 @@ type KubeSecretReference struct { // InfisicalSecretSpec defines the desired state of InfisicalSecret type InfisicalSecretSpec struct { - ServiceTokenSecret KubeSecretReference `json:"serviceTokenSecret,omitempty"` - ManagedSecret KubeSecretReference `json:"managedSecret,omitempty"` + InfisicalToken KubeSecretReference `json:"infisicalToken,omitempty"` + ManagedSecret KubeSecretReference `json:"managedSecret,omitempty"` // The Infisical project id // +kubebuilder:validation:Required diff --git a/k8-operator/api/v1alpha1/zz_generated.deepcopy.go b/k8-operator/api/v1alpha1/zz_generated.deepcopy.go index 4dc424a52..e6053ba74 100644 --- a/k8-operator/api/v1alpha1/zz_generated.deepcopy.go +++ b/k8-operator/api/v1alpha1/zz_generated.deepcopy.go @@ -88,7 +88,7 @@ func (in *InfisicalSecretList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *InfisicalSecretSpec) DeepCopyInto(out *InfisicalSecretSpec) { *out = *in - out.ServiceTokenSecret = in.ServiceTokenSecret + out.InfisicalToken = in.InfisicalToken out.ManagedSecret = in.ManagedSecret } diff --git a/k8-operator/config/crd/bases/secrets.infisical.com_infisicalsecrets.yaml b/k8-operator/config/crd/bases/secrets.infisical.com_infisicalsecrets.yaml index a3196c35c..948c9d4b4 100644 --- a/k8-operator/config/crd/bases/secrets.infisical.com_infisicalsecrets.yaml +++ b/k8-operator/config/crd/bases/secrets.infisical.com_infisicalsecrets.yaml @@ -35,13 +35,108 @@ spec: spec: description: InfisicalSecretSpec defines the desired state of InfisicalSecret properties: - foo: - description: Foo is an example field of InfisicalSecret. Edit infisicalsecret_types.go - to remove/update + environment: + description: The Infisical environment such as dev, prod, testing + type: string + infisicalToken: + properties: + name: + description: The name of the Kubernetes Secret + type: string + namespace: + description: The name space where the Kubernetes Secret is located + type: string + required: + - name + type: object + managedSecret: + properties: + name: + description: The name of the Kubernetes Secret + type: string + namespace: + description: The name space where the Kubernetes Secret is located + type: string + required: + - name + type: object + projectId: + description: The Infisical project id type: string type: object status: description: InfisicalSecretStatus defines the observed state of InfisicalSecret + properties: + conditions: + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + required: + - conditions type: object type: object served: true diff --git a/k8-operator/config/rbac/role.yaml b/k8-operator/config/rbac/role.yaml index aaf5d1cb9..0aa618262 100644 --- a/k8-operator/config/rbac/role.yaml +++ b/k8-operator/config/rbac/role.yaml @@ -5,6 +5,26 @@ metadata: creationTimestamp: null name: manager-role rules: +- apiGroups: + - "" + resources: + - secrets + verbs: + - create + - delete + - get + - list + - update + - watch +- apiGroups: + - apps + resources: + - deployments + verbs: + - get + - list + - update + - watch - apiGroups: - secrets.infisical.com resources: diff --git a/k8-operator/controllers/infisicalsecret_controller.go b/k8-operator/controllers/infisicalsecret_controller.go index 2bb74625d..dccb7cd8d 100644 --- a/k8-operator/controllers/infisicalsecret_controller.go +++ b/k8-operator/controllers/infisicalsecret_controller.go @@ -2,12 +2,15 @@ package controllers import ( "context" + "time" + "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/log" + "github.com/Infisical/infisical/k8-operator/api/v1alpha1" secretsv1alpha1 "github.com/Infisical/infisical/k8-operator/api/v1alpha1" ) @@ -20,20 +23,43 @@ type InfisicalSecretReconciler struct { //+kubebuilder:rbac:groups=secrets.infisical.com,resources=infisicalsecrets,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=secrets.infisical.com,resources=infisicalsecrets/status,verbs=get;update;patch //+kubebuilder:rbac:groups=secrets.infisical.com,resources=infisicalsecrets/finalizers,verbs=update +//+kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;delete +//+kubebuilder:rbac:groups=apps,resources=deployments,verbs=list;watch;get;update // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. -// TODO(user): Modify the Reconcile function to compare the state specified by -// the InfisicalSecret object against the actual cluster state, and then -// perform operations to make the cluster state reflect the state specified by -// the user. -// // For more details, check Reconcile and its Result here: // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.13.1/pkg/reconcile func (r *InfisicalSecretReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - _ = log.FromContext(ctx) + log := log.FromContext(ctx) - // TODO(user): your logic here + var infisicalSecretCR v1alpha1.InfisicalSecret + err := r.Get(ctx, req.NamespacedName, &infisicalSecretCR) + + if err != nil { + if errors.IsNotFound(err) { + log.Info("Infisical Secret not found") + return ctrl.Result{}, nil + } else { + log.Error(err, "Unable to fetch Infisical Secret from cluster. Will retry") + return ctrl.Result{ + RequeueAfter: time.Minute, + }, nil + } + } + + // Check if the resource is already marked for deletion + if infisicalSecretCR.GetDeletionTimestamp() != nil { + return ctrl.Result{}, nil + } + + err = r.ReconcileInfisicalSecret(ctx, infisicalSecretCR) + if err != nil { + log.Error(err, "Unable to reconcile Infisical Secret and will try again") + return ctrl.Result{ + RequeueAfter: time.Minute, + }, nil + } return ctrl.Result{}, nil } diff --git a/k8-operator/controllers/infisicalsecret_helper.go b/k8-operator/controllers/infisicalsecret_helper.go new file mode 100644 index 000000000..c2af793c3 --- /dev/null +++ b/k8-operator/controllers/infisicalsecret_helper.go @@ -0,0 +1,113 @@ +package controllers + +import ( + "context" + "fmt" + + "github.com/Infisical/infisical/k8-operator/api/v1alpha1" + api "github.com/Infisical/infisical/k8-operator/packages/api" + models "github.com/Infisical/infisical/k8-operator/packages/models" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +const INFISICAL_TOKEN_SECRET_KEY_NAME = "infisicalToken" + +func (r *InfisicalSecretReconciler) GetKubeSecretByNamespacedName(ctx context.Context, namespacedName types.NamespacedName) (*corev1.Secret, error) { + kubeSecret := &corev1.Secret{} + err := r.Client.Get(ctx, namespacedName, kubeSecret) + if err != nil { + kubeSecret = nil + } + + return kubeSecret, err +} + +func (r *InfisicalSecretReconciler) GetInfisicalToken(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret) (string, error) { + tokenSecret, err := r.GetKubeSecretByNamespacedName(ctx, types.NamespacedName{ + Namespace: infisicalSecret.Spec.ManagedSecret.Namespace, + Name: infisicalSecret.Spec.ManagedSecret.Name, + }) + + if err != nil { + return "", fmt.Errorf("failed to read infisical token secret from secret named [%s] in namespace [%s]: with error [%w]", infisicalSecret.Spec.ManagedSecret.Name, infisicalSecret.Spec.ManagedSecret.Namespace, err) + } + + infisicalServiceToken := tokenSecret.Data[INFISICAL_TOKEN_SECRET_KEY_NAME] + if infisicalServiceToken == nil { + return "", fmt.Errorf("the Infisical token is not set in the Kubernetes secret. Please add the key [%s] with the corresponding token value.", INFISICAL_TOKEN_SECRET_KEY_NAME) + } + + return string(infisicalServiceToken), nil +} + +func (r *InfisicalSecretReconciler) CreateInfisicalManagedKubeSecret(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret, secretsFromAPI []models.SingleEnvironmentVariable) error { + plainProcessedSecrets := make(map[string][]byte) + for _, secret := range secretsFromAPI { + plainProcessedSecrets[secret.Key] = []byte(secret.Value) // plain process + } + + // create a new secret as specified by the managed secret spec of CRD + newKubeSecretInstance := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: infisicalSecret.Spec.ManagedSecret.Name, + Namespace: infisicalSecret.Spec.ManagedSecret.Namespace, + }, + Type: "Opaque", + Data: plainProcessedSecrets, + } + + err := r.Client.Create(ctx, newKubeSecretInstance) + if err != nil { + return fmt.Errorf("unable to create the managed Kubernetes secret : %w", err) + } + + fmt.Println("Successfully created a managed Kubernetes secret with your Infisical secrets") + return nil +} + +func (r *InfisicalSecretReconciler) UpdateInfisicalManagedKubeSecret(ctx context.Context, managedKubeSecret corev1.Secret, secretsFromAPI []models.SingleEnvironmentVariable) error { + plainProcessedSecrets := make(map[string][]byte) + for _, secret := range secretsFromAPI { + plainProcessedSecrets[secret.Key] = []byte(secret.Value) + } + + managedKubeSecret.Data = plainProcessedSecrets + err := r.Client.Update(ctx, &managedKubeSecret) + if err != nil { + return fmt.Errorf("unable to update Kubernetes secret because [%w]", err) + } + + fmt.Println("successfully updated managed Kubernetes secret") + return nil +} + +func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret) error { + infisicalToken, err := r.GetInfisicalToken(ctx, infisicalSecret) + if err != nil { + return fmt.Errorf("unable to load Infisical Token from the specified Kubernetes secret with error [%w]", err) + } + + managedKubeSecret, err := r.GetKubeSecretByNamespacedName(ctx, types.NamespacedName{ + Name: infisicalSecret.Spec.ManagedSecret.Name, + Namespace: infisicalSecret.Spec.ManagedSecret.Namespace, + }) + + if err != nil && !errors.IsNotFound(err) { + return fmt.Errorf("something went wrong when fetching the managed Kubernetes secret [%w]", err) + } + + secretsFromApi, err := api.GetAllEnvironmentVariables(infisicalSecret.Spec.ProjectId, infisicalSecret.Spec.Environment, infisicalToken) + if err != nil { + return err + } + + if managedKubeSecret == nil { + return r.CreateInfisicalManagedKubeSecret(ctx, infisicalSecret, secretsFromApi) + } else { + return r.UpdateInfisicalManagedKubeSecret(ctx, *managedKubeSecret, secretsFromApi) + } + +} diff --git a/k8-operator/packages/auth/api.go b/k8-operator/packages/api/api.go similarity index 100% rename from k8-operator/packages/auth/api.go rename to k8-operator/packages/api/api.go