diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6b5e039ea..6d2016bd4 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -112,10 +112,10 @@ jobs: auth_file_path: /tmp/config.json - name: Install Cluster - uses: container-tools/kind-action@v2.0.1 + uses: container-tools/kind-action@v2.0.4 with: - version: v0.20.0 - node_image: kindest/node:v1.26.6@sha256:6e2d8b28a5b601defe327b98bd1c2d1930b49e5d8c512e1895099e4504007adb + version: v0.24.0 + node_image: kindest/node:v1.27.17@sha256:3fd82731af34efe19cd54ea5c25e882985bafa2c9baefe14f8deab1737d9fabe cpu: 3 registry: false config: ./ci/config.yaml diff --git a/internal/controller/common/utils/kubernetes/deployment.go b/internal/controller/common/utils/kubernetes/deployment.go index 7669d656a..692c5347b 100644 --- a/internal/controller/common/utils/kubernetes/deployment.go +++ b/internal/controller/common/utils/kubernetes/deployment.go @@ -2,8 +2,15 @@ package kubernetes import ( "context" + "encoding/binary" "errors" "fmt" + "hash" + "hash/fnv" + "strings" + + "k8s.io/apimachinery/pkg/util/dump" + "k8s.io/apimachinery/pkg/util/rand" v1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" @@ -36,11 +43,13 @@ func DeploymentIsRunning(ctx context.Context, cli client.Client, namespace strin } for _, d := range list.Items { - log.V(2).WithValues( + templateHash := ComputeHash(&d.Spec.Template, d.Status.CollisionCount) + log.WithValues( "namespace", d.Namespace, "name", d.Name, "generation", d.Generation, "observed", d.Status.ObservedGeneration, "conditions", d.Status.Conditions, + "templateHash", templateHash, ).Info("state") if d.Generation != d.Status.ObservedGeneration { @@ -53,7 +62,7 @@ func DeploymentIsRunning(ctx context.Context, cli client.Client, namespace strin } c = getDeploymentCondition(d.Status, v1.DeploymentProgressing) - if c == nil || c.Status != corev1.ConditionTrue || c.Reason != "NewReplicaSetAvailable" { + if c == nil || c.Status != corev1.ConditionTrue || c.Reason != "NewReplicaSetAvailable" || !strings.Contains(c.Message, templateHash) { return false, fmt.Errorf("%w(%s): %w", ErrDeploymentNotReady, d.Name, ErrNewReplicaSetNotAvailable) } } @@ -69,3 +78,28 @@ func getDeploymentCondition(status v1.DeploymentStatus, condType v1.DeploymentCo } return nil } + +// ComputeHash returns a hash value calculated from pod template and +// a collisionCount to avoid hash collision. The hash will be safe encoded to +// avoid bad words. +func ComputeHash(template *corev1.PodTemplateSpec, collisionCount *int32) string { + podTemplateSpecHasher := fnv.New32a() + DeepHashObject(podTemplateSpecHasher, *template) + + // Add collisionCount in the hash if it exists. + if collisionCount != nil { + collisionCountBytes := make([]byte, 8) + binary.LittleEndian.PutUint32(collisionCountBytes, uint32(*collisionCount)) + _, _ = podTemplateSpecHasher.Write(collisionCountBytes) + } + + return rand.SafeEncodeString(fmt.Sprint(podTemplateSpecHasher.Sum32())) +} + +// DeepHashObject writes specified object to hash using the spew library +// which follows pointers and prints actual values of the nested objects +// ensuring the hash does not change when a pointer changes. +func DeepHashObject(hasher hash.Hash, objectToWrite interface{}) { + hasher.Reset() + _, _ = fmt.Fprintf(hasher, "%v", dump.ForHash(objectToWrite)) +} diff --git a/internal/testing/kubernetes/deployment.go b/internal/testing/kubernetes/deployment.go index 5ce7dc8de..83385bf3e 100644 --- a/internal/testing/kubernetes/deployment.go +++ b/internal/testing/kubernetes/deployment.go @@ -3,6 +3,9 @@ package kubernetes import ( "context" "errors" + "fmt" + + "github.com/securesign/operator/internal/controller/common/utils/kubernetes" "github.com/securesign/operator/internal/controller/constants" v1 "k8s.io/api/apps/v1" @@ -15,10 +18,13 @@ func SetDeploymentToReady(ctx context.Context, cli client.Client, deployment *v1 return errors.New("nil deployment") } + templateHash := kubernetes.ComputeHash(&deployment.Spec.Template, deployment.Status.CollisionCount) + deployment.Status.ObservedGeneration = deployment.Generation deployment.Status.Conditions = []v1.DeploymentCondition{ {Status: corev1.ConditionTrue, Type: v1.DeploymentAvailable, Reason: constants.Ready}, - {Status: corev1.ConditionTrue, Type: v1.DeploymentProgressing, Reason: "NewReplicaSetAvailable"}, + {Status: corev1.ConditionTrue, Type: v1.DeploymentProgressing, Reason: "NewReplicaSetAvailable", + Message: fmt.Sprintf("ReplicaSet \"%s-%s\" has successfully progressed.", deployment.Name, templateHash)}, } return cli.Status().Update(ctx, deployment) }