mirror of
https://github.com/cozystack/cozystack.git
synced 2026-10-26 21:17:32 +09:00
Say that a restored copy converges its application passwords shortly after recovery, that MariaDB backups leave out the grant table, and that dropping the password field does not rewrite copies made before the upgrade: old Backup snapshots and Helm revisions still hold the password that stays live. The credentials projector comment counts only the engines that still take a password from values. Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
140 lines
4.7 KiB
Go
140 lines
4.7 KiB
Go
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package migrationcontroller
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
corev1 "k8s.io/api/core/v1"
|
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/types"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
|
|
migrationv1alpha1 "github.com/cozystack/cozystack/api/migration/v1alpha1"
|
|
)
|
|
|
|
// ProjectionError is a typed error so the reconciler can tell a misconfiguration
|
|
// that needs a human (a Secret owned by someone else) from an API hiccup that
|
|
// will clear on its own.
|
|
type ProjectionError struct {
|
|
Reason string
|
|
Message string
|
|
}
|
|
|
|
func (e *ProjectionError) Error() string { return e.Message }
|
|
|
|
// projectCredentials writes a Source's credentials into the Secret shape
|
|
// Forklift's Provider consumes, and returns the Secret's name.
|
|
//
|
|
// Tenants cannot create Secrets in Cozystack (core.cozystack.io/tenantsecrets is
|
|
// read-only at every access level), so a credentialsSecretRef field would be
|
|
// unsatisfiable for the audience this API is for: the credentials arrive on the
|
|
// Source's spec and the controller materializes them. The exposure class is the
|
|
// same one the managed databases that still take a values-supplied credential
|
|
// carry (the postgres and mariadb apps take no `users[].password`; the other
|
|
// engines still do).
|
|
//
|
|
// The projected Secret is labelled as ours and the projector refuses to write a
|
|
// pre-existing Secret that is not, so a name collision can never clobber a
|
|
// tenant's own object — it surfaces as a terminal condition instead.
|
|
func projectCredentials(ctx context.Context, c client.Client, src *migrationv1alpha1.VMImportSource) (string, error) {
|
|
return projectSecret(ctx, c, src, credentialsSecretName(src.Name), src.Spec.Credentials)
|
|
}
|
|
|
|
// projectSecret materializes one credential set into a Secret of the given
|
|
// name. Shared by the provider connection and by each host override, which
|
|
// carries its own ESXi account for the same reason: the engine authenticates
|
|
// that connection on the host itself.
|
|
func projectSecret(
|
|
ctx context.Context,
|
|
c client.Client,
|
|
src *migrationv1alpha1.VMImportSource,
|
|
name string,
|
|
creds migrationv1alpha1.ProviderCredentials,
|
|
) (string, error) {
|
|
desired := map[string][]byte{
|
|
"user": []byte(creds.Username),
|
|
"password": []byte(creds.Password),
|
|
}
|
|
// The engine's vSphere validation wants `user`, `password`, and then either
|
|
// a `cacert` or `insecureSkipVerify` parseable as a boolean. Without one of
|
|
// the latter two it marks the provider's secret invalid and never attempts
|
|
// a connection — a state that looks like a credentials problem and is not.
|
|
// Verified against the vendored engine's own validation on a live cluster.
|
|
if creds.CACert != "" {
|
|
desired["cacert"] = []byte(creds.CACert)
|
|
}
|
|
if creds.InsecureSkipVerify {
|
|
desired["insecureSkipVerify"] = []byte("true")
|
|
}
|
|
|
|
existing := &corev1.Secret{}
|
|
err := c.Get(ctx, types.NamespacedName{Namespace: src.Namespace, Name: name}, existing)
|
|
switch {
|
|
case apierrors.IsNotFound(err):
|
|
secret := &corev1.Secret{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: name,
|
|
Namespace: src.Namespace,
|
|
Labels: map[string]string{
|
|
migrationv1alpha1.ManagedByLabel: migrationv1alpha1.ManagedByValue,
|
|
},
|
|
// Forklift matches a provider's Secret by this label when it
|
|
// re-reads credentials after a Provider edit.
|
|
Annotations: map[string]string{
|
|
"createdForProviderType": string(src.Spec.Type),
|
|
},
|
|
OwnerReferences: []metav1.OwnerReference{
|
|
ownerRef(migrationv1alpha1.GroupVersion.WithKind("VMImportSource"), src.Name, src.UID),
|
|
},
|
|
},
|
|
Type: corev1.SecretTypeOpaque,
|
|
Data: desired,
|
|
}
|
|
if err := c.Create(ctx, secret); err != nil {
|
|
if apierrors.IsAlreadyExists(err) {
|
|
// Lost a race with another reconcile; the next pass adopts it.
|
|
return name, &ProjectionError{Reason: migrationv1alpha1.ReasonCredentialsMissing, Message: "credentials Secret is being created"}
|
|
}
|
|
return "", err
|
|
}
|
|
return name, nil
|
|
|
|
case err != nil:
|
|
return "", err
|
|
}
|
|
|
|
if existing.Labels[migrationv1alpha1.ManagedByLabel] != migrationv1alpha1.ManagedByValue {
|
|
return "", &ProjectionError{
|
|
Reason: migrationv1alpha1.ReasonCredentialsMissing,
|
|
Message: fmt.Sprintf(
|
|
"Secret %q already exists in this namespace and is not managed by the migration controller; "+
|
|
"rename the VMImportSource or remove the Secret", name),
|
|
}
|
|
}
|
|
|
|
if secretDataEqual(existing.Data, desired) {
|
|
return name, nil
|
|
}
|
|
existing.Data = desired
|
|
if err := c.Update(ctx, existing); err != nil {
|
|
return "", err
|
|
}
|
|
return name, nil
|
|
}
|
|
|
|
func secretDataEqual(a, b map[string][]byte) bool {
|
|
if len(a) != len(b) {
|
|
return false
|
|
}
|
|
for k, av := range a {
|
|
bv, ok := b[k]
|
|
if !ok || string(av) != string(bv) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|