8000 feat: represent revision tags using services by german1608 · Pull Request #56141 · istio/istio · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

feat: represent revision tags using services #56141

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
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
7272f6c
first iteration of using services and mutatingwebhooks as tags
german1608 Apr 30, 2025
25369b5
include namespaces
german1608 Apr 30, 2025
a844ca6
log warnings on conflict
german1608 Apr 30, 2025
4634344
rename revision-tags to revision-tags-mwc.yaml
german1608 Apr 30, 2025
25cd5a8
Create service tags on helm istiod chart
german1608 Apr 30, 2025
2ea0dbd
initial iteration for istioctl tag set
german1608 Apr 30, 2025
17e8228
include IstioNamespace in GenerateOptions
german1608 Apr 30, 2025
e801b75
Update signature of Generate to return a list of objects to generate
german1608 Apr 30, 2025
9e8daf2
consider service if running ambient
german1608 Apr 30, 2025
def2910
go back to strings when returning
german1608 Apr 30, 2025
fb08f9f
go back to string completely, delete TagResources
german1608 Apr 30, 2025
84c8036
rename service object
german1608 May 1, 2025
afe7c31
move break to correct line
german1608 May 1, 2025
aa30e06
implement istioctl tag remove
german1608 May 2, 2025
8000
bc07b99
add uniqTagsFromServices and uniqTagsFromWebhooks
german1608 May 6, 2025
a110ce1
istioctl tag list consider services
german1608 May 6, 2025
24e997a
add operatorManageWebhooks guard to revision-tags-mwc.yaml
german1608 May 7, 2025
073cd3b
rename revision-tags.yaml to revision-tags-svc.yaml
german1608 May 7, 2025
daf14e4
refactor delete test case
german1608 May 7, 2025
cf5d4a0
test istioctl tag list
german1608 May 7, 2025
5032ff8
remove ambient check, create and manage both resources at the same time
german1608 May 8, 2025
7355e44
wip test cases
german1608 May 12, 2025
d363126
remove controlplane mode
german1608 May 12, 2025
a9b0c57
fix test
german1608 May 12, 2025
31951ca
my own code review
german1608 May 12, 2025
d523a99
fix compilation error
german1608 May 12, 2025
ee5c292
fix lint
german1608 May 12, 2025
d60799b
wip integ-helm_istio
german1608 May 13, 2025
b9a7b34
add app=istiod label filter
german1608 May 14, 2025
0b0537e
merge override labels in generate.go
german1608 May 14, 2025
21c8037
add release note
german1608 May 14, 2025
97ff49e
set owner labels to revision service tag
german1608 May 15, 2025
4a40763
rollback changes on e2e for helm
german1608 May 15, 2025
177ac99
fix helm e2e
german1608 May 15, 2025
6588467
use servicesAfters instead of webhooksAfter in tag_test.go assertion log
german1608 Jun 7, 2025
245dfac
Remove unnecessary deprecation message
german1608 Jun 9, 2025
c40ad5a
refactor tag_test.go to reduce diff size
german1608 Jun 24, 2025
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
183 changes: 157 additions & 26 deletions istioctl/pkg/tag/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,15 @@ import (
"strings"

admitv1 "k8s.io/api/admissionregistration/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apimachinery/pkg/runtime/serializer/json"
"k8s.io/client-go/kubernetes"

"istio.io/api/label"
"istio.io/istio/operator/pkg/helm"
"istio.io/istio/operator/pkg/render"
"istio.io/istio/operator/pkg/values"
Expand Down Expand Up @@ -86,45 +89,33 @@ type GenerateOptions struct {
// UserManaged indicates whether the revision tag is user managed.
// If true, the revision tag will not be affected by the installer.
UserManaged bool
// IstioNamespace indicates the namespace of the istio installation.
IstioNamespace string
}

// Generate generates the manifests for a revision tag pointed the given revision.
func Generate(ctx context.Context, client kube.Client, opts *GenerateOptions, istioNS string) (string, error) {
func Generate(ctx context.Context, client kube.Client, opts *GenerateOptions) (string, error) {
// abort if there exists a revision with the target tag name
revWebhookCollisions, err := GetWebhooksWithRevision(ctx, client.Kube(), opts.Tag)
err := checkTagNameCollidesWithRevisionName(ctx, client.Kube(), opts)
if err != nil {
return "", err
}
if !opts.Generate && !opts.Overwrite &&
len(revWebhookCollisions) > 0 && opts.Tag != DefaultRevisionName {
return "", fmt.Errorf("cannot create revision tag %q: found existing control plane revision with same name", opts.Tag)
}

// find canonical revision webhook to base our tag webhook off of
revWebhooks, err := GetWebhooksWithRevision(ctx, client.Kube(), opts.Revision)
canonWebhook, err := checkControlPlaneExistenceOrDuplicate(ctx, client.Kube(), opts)
if err != nil {
return "", err
}
if len(revWebhooks) == 0 {
return "", fmt.Errorf("cannot modify tag: cannot find MutatingWebhookConfiguration with revision %q", opts.Revision)
}
if len(revWebhooks) > 1 {
return "", fmt.Errorf("cannot modify tag: found multiple canonical webhooks with revision %q", opts.Revision)
}

whs, err := GetWebhooksWithTag(ctx, client.Kube(), opts.Tag)
err = checkTagDuplicate(ctx, client.Kube(), opts)
if err != nil {
return "", err
}
if len(whs) > 0 && !opts.Overwrite {
return "", fmt.Errorf("revision tag %q already exists, and --overwrite is false", opts.Tag)
}

tagWhConfig, err := tagWebhookConfigFromCanonicalWebhook(revWebhooks[0], opts.Tag, istioNS)
tagWhConfig, err := tagWebhookConfigFromCanonicalWebhook(*canonWebhook, opts.Tag, opts.IstioNamespace)
if err != nil {
return "", fmt.Errorf("failed to create tag webhook config: %w", err)
}
tagWhYAML, err := generateMutatingWebhook(tagWhConfig, opts)
var vwhYAML string
if err != nil {
return "", fmt.Errorf("failed to create tag webhook: %w", err)
}
Expand All @@ -146,16 +137,103 @@ func Generate(ctx context.Context, client kube.Client, opts *GenerateOptions, is
return "", fmt.Errorf("failed to create validating webhook config: %w", err)
}

vwhYAML, err := generateValidatingWebhook(validationWhConfig, opts)
vwhYAML, err = generateValidatingWebhook(validationWhConfig, opts)
if err != nil {
return "", fmt.Errorf("failed to create validating webhook: %w", err)
}
tagWhYAML = fmt.Sprintf(`%s
---
%s`, tagWhYAML, vwhYAML)
}
tagServiceYAML, err := generateTagService(opts)
if err != nil {
return "", err
}

return tagWhYAML, nil
resources := []string{
tagWhYAML,
vwhYAML,
tagServiceYAML,
}
resourcesStrings := []string{}
for _, resource := range resources {
if resource == "" {
continue
}
resourcesStrings = append(resourcesStrings, resource)
}

return strings.Join(resourcesStrings, "\n---\n"), nil
}

// checkTagNameCollidesWithRevisionName returns an error if user attempts to
// override a revision using a tag name
func checkTagNameCollidesWithRevisionName(
ctx context.Context,
client kubernetes.Interface,
opts *GenerateOptions,
) error {
if opts.Generate || opts.Overwrite || opts.Tag == DefaultRevisionName {
return nil
}
revServiceCollisions, err := GetServicesWithRevision(ctx, client, opts.IstioNamespace, opts.Tag)
if err != nil {
return err
}
// abort if there exists a revision with the target tag name
revWebhookCollisions, err := GetWebhooksWithRevision(ctx, client, opts.Tag)
if err != nil {
return err
}
if len(revWebhookCollisions) > 0 || len(revServiceCollisions) > 0 {
return fmt.Errorf("cannot create revision tag %q: found existing control plane revision with same name", opts.Tag)
}
return nil
}

func checkControlPlaneExistenceOrDuplicate(
ctx context.Context,
client kubernetes.Interface,
opts *GenerateOptions,
) (*admitv1.MutatingWebhookConfiguration, error) {
revServices, err := GetServicesWithRevision(ctx, client, opts.IstioNamespace, opts.Revision)
if err != nil {
return nil, err
}

if len(revServices) > 1 {
return nil, fmt.Errorf("cannot modify tag: found multiple canonical services with revision %q in namespace %q", opts.Revision, opts.IstioNamespace)
}
revWebhooks, err := GetWebhooksWithRevision(ctx, client, opts.Revision)
if err != nil {
return nil, err
}
if len(revWebhooks) == 0 && len(revServices) == 0 {
return nil, fmt.Errorf("cannot modify tag: cannot find MutatingWebhookConfiguration or Service with revision %q", opts.Revision)
}
if len(revWebhooks) > 1 || len(revServices) > 1 {
return nil, fmt.Errorf("cannot modify tag: found multiple canonical webhooks or services with revision %q", opts.Revision)
}
return &revWebhooks[0], nil
}

func checkTagDuplicate(
ctx context.Context,
client kubernetes.Interface,
opts *GenerateOptions,
) error {
if opts.Overwrite {
return nil
}
tagServices, err := GetServicesWithTag(ctx, client, opts.IstioNamespace, opts.Tag)
if err != nil {
return err
}
whs, err := GetWebhooksWithTag(ctx, client, opts.Tag)
if err != nil {
return err
}
if len(whs) > 0 || len(tagServices) > 0 {
return fmt.Errorf("revision tag %q already exists, and --overwrite is false", opts.Tag)
}
return nil
}

func fixWhConfig(client kube.Client, whConfig *tagWebhookConfig) (*tagWebhookConfig, error) {
Expand Down Expand Up @@ -250,7 +328,6 @@ func generateValidatingWebhook(config *tagWebhookConfig, opts *GenerateOptions)
decodedWh.Webhooks[i].FailurePolicy = failurePolicy
}
}

whBuf := new(bytes.Buffer)
if err = serializer.Encode(decodedWh, whBuf); err != nil {
return "", err
Expand All @@ -272,6 +349,60 @@ func generateLabels(whLabels, curLabels, customLabels map[string]string, userMan
return whLabels
}

func generateTagService(opts *GenerateOptions) (string, error) {
flags := []string{
"installPackagePath=" + opts.ManifestsPath,
"profile=empty",
"components.pilot.enabled=true",
"revision=" + opts.Revision,
"values.revisionTags.[0]=" + opts.Tag,
"values.global.istioNamespace=" + opts.IstioNamespace,
}

mfs, _, err := render.GenerateManifest(nil, flags, false, nil, nil)
if err != nil {
return "", err
}
var tagServiceYaml string
for _, mf := range mfs {
for _, m := range mf.Manifests {
tag := m.GetLabels()[label.IoIstioTag.Name]
if m.GetKind() == "Service" && tag == opts.Tag {
tagServiceYaml = m.Content
break
}
}
}
if tagServiceYaml == "" {
return "", fmt.Errorf("could not find Service tag in manifests")
}

scheme := runtime.NewScheme()
codecFactory := serializer.NewCodecFactory(scheme)
deserializer := codecFactory.UniversalDeserializer()
serializer := json.NewSerializerWithOptions(
json.DefaultMetaFactory, nil, nil, json.SerializerOptions{
Yaml: true,
Pretty: true,
Strict: true,
})

whService, _, err := deserializer.Decode([]byte(tagServiceYaml), nil, &corev1.Service{})
if err != nil {
return "", fmt.Errorf("could not decode generated webhook: %w", err)
}
decodedSvc := whService.(*corev1.Service)

decodedSvc.Labels = generateLabels(decodedSvc.Labels, map[string]string{}, opts.CustomLabels, opts.UserManaged)

svcBuf := new(bytes.Buffer)
if err = serializer.Encode(decodedSvc, svcBuf); err != nil {
return "", err
}

return svcBuf.String(), nil
}

// generateMutatingWebhook renders a mutating webhook configuration from the given tagWebhookConfig.
func generateMutatingWebhook(config *tagWebhookConfig, opts *GenerateOptions) (string, error) {
flags := []string{
Expand Down
45 changes: 41 additions & 4 deletions istioctl/pkg/tag/generate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"testing"

admitv1 "k8s.io/api/admissionregistration/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
Expand Down Expand Up @@ -464,17 +465,53 @@ func TestGenerateMutatingWebhook(t *testing.T) {
}
}

func TestGenerateTagService(t *testing.T) {
scheme := runtime.NewScheme()
codecFactory := serializer.NewCodecFactory(scheme)
deserializer := codecFactory.UniversalDeserializer()

tag := "canary"
revision := "revision"
opts := &GenerateOptions{
IstioNamespace: "istio-system",
Tag: tag,
Revision: revision,
}
svcYAML, err := generateTagService(opts)
if err != nil {
t.Fatalf("Could not generate tag service: %q", err)
}

svcObject, _, err := deserializer.Decode([]byte(svcYAML), nil, &corev1.Service{})
if err != nil {
t.Fatalf("Could not parse service: %q", svcYAML)
}

service := svcObject.(*corev1.Service)

labels := service.GetLabels()
assert.Equal(t, labels[label.IoIstioRev.Name], revision, "Tag service does not have expected revision")
assert.Equal(t, labels[label.IoIstioTag.Name], tag, "Tag service does not have expected tag")
assert.Equal(t, service.GetName(), "istiod-revision-tag-canary", "Tag service does not have expected name")
assert.Equal(t, service.GetNamespace(), "istio-system", "Tag service does not have expected namespace")

selector := service.Spec.Selector

assert.Equal(t, selector[label.IoIstioRev.Name], revision, "Selector istio.io/rev does not match expected value")
}

func testGenerateOption(t *testing.T, generate bool, assertFunc func(*testing.T, []admitv1.MutatingWebhook, []admitv1.MutatingWebhook)) {
defaultWh := defaultRevisionCanonicalWebhook.DeepCopy()
fakeClient := kube.NewFakeClient(defaultWh)

opts := &GenerateOptions{
Generate: generate,
Tag: "default",
Revision: "default",
Generate: generate,
Tag: "default",
Revision: "default",
IstioNamespace: "istio-system",
}

_, err := Generate(context.TODO(), fakeClient, opts, "istio-system")
_, err := Generate(context.TODO(), fakeClient, opts)
assert.NoError(t, err)

wh, err := fakeClient.Kube().AdmissionregistrationV1().MutatingWebhookConfigurations().
Expand Down
Loading
0