Skip to content

[K8s Plugin] Implement generateVariantWorkloadManifests #5864

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
May 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions pkg/app/pipedv1/plugin/kubernetes/deployment/misc.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
"fmt"
"strings"

appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

"github.com/pipe-cd/pipecd/pkg/app/pipedv1/plugin/kubernetes/provider"
)
Expand Down Expand Up @@ -100,6 +102,111 @@
return manifests, nil
}

// generateVariantWorkloadManifests generates Workload manifests for the specified variant.
// It duplicates the given Workload manifests, adds a name suffix, sets the variant label to the selector,
// and updates the ENV references in containers to use canary's ConfigMaps and Secrets.
func generateVariantWorkloadManifests(workloads, configmaps, secrets []provider.Manifest, variantLabel, variant, nameSuffix string, replicasCalculator func(*int32) int32) ([]provider.Manifest, error) {
manifests := make([]provider.Manifest, 0, len(workloads))

cmNames := make(map[string]struct{}, len(configmaps))
for _, cm := range configmaps {
cmNames[cm.Name()] = struct{}{}
}

secretNames := make(map[string]struct{}, len(secrets))
for _, secret := range secrets {
secretNames[secret.Name()] = struct{}{}
}

updateContainers := func(containers []corev1.Container) {
for _, container := range containers {
for _, env := range container.Env {
if v := env.ValueFrom; v != nil {
if ref := v.ConfigMapKeyRef; ref != nil {
if _, ok := cmNames[ref.Name]; ok {
ref.Name = makeSuffixedName(ref.Name, nameSuffix)
}
}
if ref := v.SecretKeyRef; ref != nil {
if _, ok := secretNames[ref.Name]; ok {
ref.Name = makeSuffixedName(ref.Name, nameSuffix)
}
}
}
}
for _, envFrom := range container.EnvFrom {
if ref := envFrom.ConfigMapRef; ref != nil {
if _, ok := cmNames[ref.Name]; ok {
ref.Name = makeSuffixedName(ref.Name, nameSuffix)
}
}
if ref := envFrom.SecretRef; ref != nil {
if _, ok := secretNames[ref.Name]; ok {
ref.Name = makeSuffixedName(ref.Name, nameSuffix)
}
}
}
}
}

updatePod := func(pod *corev1.PodTemplateSpec) {
// Add variant labels.
if pod.Labels == nil {
pod.Labels = map[string]string{}
}

Check warning on line 156 in pkg/app/pipedv1/plugin/kubernetes/deployment/misc.go

View check run for this annotation

Codecov / codecov/patch

pkg/app/pipedv1/plugin/kubernetes/deployment/misc.go#L155-L156

Added lines #L155 - L156 were not covered by tests
pod.Labels[variantLabel] = variant

// Update volumes to use canary's ConfigMaps and Secrets.
for i := range pod.Spec.Volumes {
if cm := pod.Spec.Volumes[i].ConfigMap; cm != nil {
if _, ok := cmNames[cm.Name]; ok {
cm.Name = makeSuffixedName(cm.Name, nameSuffix)
}
}
if s := pod.Spec.Volumes[i].Secret; s != nil {
if _, ok := secretNames[s.SecretName]; ok {
s.SecretName = makeSuffixedName(s.SecretName, nameSuffix)
}
}
}

// Update ENV references in containers.
updateContainers(pod.Spec.InitContainers)
updateContainers(pod.Spec.Containers)
}

updateDeployment := func(d *appsv1.Deployment) {
d.Name = makeSuffixedName(d.Name, nameSuffix)
if replicasCalculator != nil {
replicas := replicasCalculator(d.Spec.Replicas)
d.Spec.Replicas = &replicas
}
d.Spec.Selector = metav1.AddLabelToSelector(d.Spec.Selector, variantLabel, variant)
updatePod(&d.Spec.Template)
}

for _, m := range workloads {
switch m.Kind() {
case provider.KindDeployment:
d := &appsv1.Deployment{}
if err := m.ConvertToStructuredObject(d); err != nil {
return nil, err
}

Check warning on line 194 in pkg/app/pipedv1/plugin/kubernetes/deployment/misc.go

View check run for this annotation

Codecov / codecov/patch

pkg/app/pipedv1/plugin/kubernetes/deployment/misc.go#L193-L194

Added lines #L193 - L194 were not covered by tests
updateDeployment(d)
manifest, err := provider.FromStructuredObject(d)
if err != nil {
return nil, err
}

Check warning on line 199 in pkg/app/pipedv1/plugin/kubernetes/deployment/misc.go

View check run for this annotation

Codecov / codecov/patch

pkg/app/pipedv1/plugin/kubernetes/deployment/misc.go#L198-L199

Added lines #L198 - L199 were not covered by tests
manifests = append(manifests, manifest)

default:
return nil, fmt.Errorf("unsupported workload kind %s", m.Kind())

Check warning on line 203 in pkg/app/pipedv1/plugin/kubernetes/deployment/misc.go

View check run for this annotation

Codecov / codecov/patch

pkg/app/pipedv1/plugin/kubernetes/deployment/misc.go#L202-L203

Added lines #L202 - L203 were not covered by tests
}
}

return manifests, nil
}

func makeSuffixedName(name, suffix string) string {
if suffix != "" {
return name + "-" + suffix
Expand Down
62 changes: 62 additions & 0 deletions pkg/app/pipedv1/plugin/kubernetes/deployment/misc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,68 @@ spec:
}
}

func TestGenerateVariantWorkloadManifests(t *testing.T) {
t.Parallel()

const (
variantLabel = "pipecd.dev/variant"
canaryVariant = "canary-variant"
)
testcases := []struct {
name string
manifestsFile string
configmapsFile string
secretsFile string
}{
{
name: "No configmap and secret",
manifestsFile: "testdata/variant_workload_manifests/no-config-deployments.yaml",
},
{
name: "Has configmap and secret",
manifestsFile: "testdata/variant_workload_manifests/deployments.yaml",
configmapsFile: "testdata/variant_workload_manifests/configmaps.yaml",
secretsFile: "testdata/variant_workload_manifests/secrets.yaml",
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()

manifests, err := provider.LoadManifestsFromYAMLFile(tc.manifestsFile)
require.NoError(t, err)
require.Equal(t, 2, len(manifests))

var configmaps, secrets []provider.Manifest
if tc.configmapsFile != "" {
configmaps, err = provider.LoadManifestsFromYAMLFile(tc.configmapsFile)
require.NoError(t, err)
}
if tc.secretsFile != "" {
secrets, err = provider.LoadManifestsFromYAMLFile(tc.secretsFile)
require.NoError(t, err)
}

calculator := func(r *int32) int32 {
return *r - 1
}
generatedManifests, err := generateVariantWorkloadManifests(
manifests[:1],
configmaps,
secrets,
variantLabel,
canaryVariant,
"canary",
calculator,
)
require.NoError(t, err)
require.Equal(t, 1, len(generatedManifests))

assert.Equal(t, manifests[1], generatedManifests[0])
})
}
}

func TestAddVariantLabelsAndAnnotations(t *testing.T) {
t.Parallel()

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: configmap-name-2
data:
piped-config.yaml: |-
data
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: simple
spec:
replicas: 10
selector:
matchLabels:
app: simple
template:
metadata:
labels:
app: simple
spec:
initContainers:
- image: gcr.io/pipecd/init:v0.1.0
name: helloworld
ports:
- containerPort: 9085
protocol: TCP
env:
- name: CONFIG_ENV
valueFrom:
configMapKeyRef:
key: key
name: configmap-name-2
- name: SECRET_ENV
valueFrom:
secretKeyRef:
key: key
name: secret-name-1
envFrom:
- configMapRef:
name: configmap-name-2
- secretRef:
name: secret-name-1
containers:
- args:
- server
image: gcr.io/pipecd/helloworld:v0.1.0-73-ge191187
imagePullPolicy: IfNotPresent
name: helloworld
ports:
- containerPort: 9085
protocol: TCP
env:
- name: CONFIG_ENV
valueFrom:
configMapKeyRef:
key: key
name: configmap-name-2
configMapKeyRef:
key: key2
name: not-managed-config-map
- name: SECRET_ENV
valueFrom:
secretKeyRef:
key: key
name: secret-name-1
envFrom:
- configMapRef:
name: configmap-name-2
- secretRef:
name: secret-name-1
resources: {}
volumes:
- name: secret-1
secret:
defaultMode: 256
secretName: secret-name-1
- name: secret-2
secret:
defaultMode: 256
secretName: secret-name-2
- configMap:
defaultMode: 420
name: configmap-name-1
name: config-1
- configMap:
defaultMode: 420
name: configmap-name-2
name: config-2
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: simple-canary
creationTimestamp:
spec:
replicas: 9
selector:
matchLabels:
app: simple
pipecd.dev/variant: canary-variant
strategy: {}
template:
metadata:
creationTimestamp:
labels:
app: simple
pipecd.dev/variant: canary-variant
spec:
initContainers:
- image: gcr.io/pipecd/init:v0.1.0
name: helloworld
ports:
- containerPort: 9085
protocol: TCP
env:
- name: CONFIG_ENV
valueFrom:
configMapKeyRef:
key: key
name: configmap-name-2-canary
- name: SECRET_ENV
valueFrom:
secretKeyRef:
key: key
name: secret-name-1-canary
envFrom:
- configMapRef:
name: configmap-name-2-canary
- secretRef:
name: secret-name-1-canary
resources: {}
containers:
- args:
- server
image: gcr.io/pipecd/helloworld:v0.1.0-73-ge191187
imagePullPolicy: IfNotPresent
name: helloworld
ports:
- containerPort: 9085
protocol: TCP
env:
- name: CONFIG_ENV
valueFrom:
configMapKeyRef:
key: key
name: configmap-name-2-canary
configMapKeyRef:
key: key2
name: not-managed-config-map
- name: SECRET_ENV
valueFrom:
secretKeyRef:
key: key
name: secret-name-1-canary
envFrom:
- configMapRef:
name: configmap-name-2-canary
- secretRef:
name: secret-name-1-canary
resources: {}
volumes:
- name: secret-1
secret:
defaultMode: 256
secretName: secret-name-1-canary
- name: secret-2
secret:
defaultMode: 256
secretName: secret-name-2
- configMap:
defaultMode: 420
name: configmap-name-1
name: config-1
- configMap:
defaultMode: 420
name: configmap-name-2-canary
name: config-2
status: {}
Loading