azure: Add Workload Identity support
by hakman ยท April 17, 2026
Overview
Motivation
C4 Context
kops is a Kubernetes cluster lifecycle management tool (the System). This PR extends kops's Azure provider to support the Workload Identity authentication model on Azure Kubernetes clusters, allowing in-cluster service accounts to federate with Azure Active Directory for secretless access to Azure APIs.
The affected Containers are the kops CLI binary (cmd/kops), the Azure cloud provider backend (upup/pkg/fi/cloudup/azure), the VFS Azure Blob layer (util/pkg/vfs/azureblob.go), and the addon manifests served to clusters (upup/models/cloudup/resources/addons). New Azure SDK vendor code (armmsi) is also introduced as a dependency container.
At the Component level, three new Azure task components are added โ ManagedIdentity, FederatedIdentityCredential, and an updated RoleAssignment โ alongside a new WorkloadIdentity model builder (pkg/model/azuremodel/workloadidentity.go). The OIDC issuer discovery component (pkg/model/issuerdiscovery.go) is extended to publish discovery documents to Azure Blob Storage, and the KubeControllerManager and cloud-node-manager addon templates are updated to consume workload identity annotations and projected token volumes.
Cluster API Types & Version Conversion
WorkloadIdentityClientID field to AzureSpec across the internal API type (pkg/apis/kops/componentconfig.go), the v1alpha2 and v1alpha3 versioned types, and updates the auto-generated conversion functions so the field round-trips correctly across API versions. Also adds AzureWorkloadIdentityName() and AzureNetworkName() helper methods to the Cluster type to centralise name derivation.Cluster API Types & Version Conversion โ Key Signatures
| Name | File | What it does |
|---|---|---|
AzureNetworkName |
pkg/apis/kops/cluster.go | Returns the Azure Virtual Network name for the cluster, using the shared NetworkID if set, or falling back to the cluster name. |
AzureNetworkSecurityGroupName |
pkg/apis/kops/cluster.go | Returns the NSG name by delegating entirely to AzureNetworkName(), removing the previously duplicated logic. |
AzureWorkloadIdentityName |
pkg/apis/kops/cluster.go | Returns the User-Assigned Managed Identity name by prepending 'wi-' to the cluster name with dots replaced by hyphens. |
WorkloadIdentityClientID (field in AzureSpec) |
pkg/apis/kops/componentconfig.go | New string field that stores the client ID of the User-Assigned Managed Identity used for Azure Workload Identity, auto-populated when UseServiceAccountExternalPermissions is enabled. |
autoConvert_v1alpha2_AzureSpec_To_kops_AzureSpec |
pkg/apis/kops/v1alpha2/zz_generated.conversion.go | Auto-generated function that now also copies WorkloadIdentityClientID from the v1alpha2 versioned type to the internal type. |
autoConvert_kops_AzureSpec_To_v1alpha2_AzureSpec |
pkg/apis/kops/v1alpha2/zz_generated.conversion.go | Auto-generated function that now also copies WorkloadIdentityClientID from the internal type back to the v1alpha2 versioned type. |
autoConvert_v1alpha3_AzureSpec_To_kops_AzureSpec |
pkg/apis/kops/v1alpha3/zz_generated.conversion.go | Auto-generated function that now also copies WorkloadIdentityClientID from the v1alpha3 versioned type to the internal type. |
autoConvert_kops_AzureSpec_To_v1alpha3_AzureSpec |
pkg/apis/kops/v1alpha3/zz_generated.conversion.go | Auto-generated function that now also copies WorkloadIdentityClientID from the internal type back to the v1alpha3 versioned type. |
Cluster API Types & Version Conversion โ Walkthrough
The primary data-model change is the addition of WorkloadIdentityClientID string to the AzureSpec struct in the internal API (componentconfig.go) and both versioned API packages (v1alpha2, v1alpha3); all three definitions carry identical JSON tags and doc-comments, which is the correct pattern for kops' multi-version API.
The auto-generated conversion files (zz_generated.conversion.go) are updated in both directions for each API version, ensuring the field round-trips without loss when a cluster object is read from or written to any supported API version โ this is critical for backwards compatibility.
AzureNetworkSecurityGroupName() is refactored to delegate to the new AzureNetworkName() helper rather than containing its own copy of the NetworkID-or-cluster-name logic; this eliminates a subtle duplication risk where the two functions could drift apart in future changes.
AzureWorkloadIdentityName() centralises the name derivation rule ("wi-" + strings.ReplaceAll(c.Name, ".", "-")) so that both the pre-flight check in apply_cluster.go and the task-graph entry in azuremodel are guaranteed to produce the same string โ previously any inline derivation at two call sites was a correctness hazard.
The naming transformation in AzureWorkloadIdentityName() replaces dots with hyphens, which is important because Azure resource names generally do not allow dots, but the rule is applied only to dots; other characters that are valid in Kubernetes cluster names (e.g. underscores, colons) are not sanitised, which could be a latent issue for non-standard cluster names.
Cluster API Types & Version Conversion
pkg/apis/kops/cluster.go modified
@@ -18,6 +18,7 @@ package kops
import (
"fmt"
+ "strings"
"github.com/blang/semver/v4"
corev1 "k8s.io/api/core/v1"
@@ -922,15 +923,31 @@ func (c *Cluster) AzureRouteTableName() string {
return c.Name
}
-// AzureNetworkSecurityGroupName returns the name of the network security group for the cluster.
-// The NSG shares its name with the virtual network.
-func (c *Cluster) AzureNetworkSecurityGroupName() string {
+// AzureNetworkName returns the name of the Azure Virtual Network for the cluster.
+// If Networking.NetworkID is set (shared network), that is used; otherwise the
+// cluster name is used.
+func (c *Cluster) AzureNetworkName() string {
if c.Spec.Networking.NetworkID != "" {
return c.Spec.Networking.NetworkID
}
return c.Name
}
+// AzureNetworkSecurityGroupName returns the name of the network security group for the cluster.
+// The NSG shares its name with the virtual network; callers relying on this invariant
+// should prefer this helper over re-deriving the name inline.
+func (c *Cluster) AzureNetworkSecurityGroupName() string {
+ return c.AzureNetworkName()
+}
+
+// AzureWorkloadIdentityName returns the name of the User-Assigned Managed Identity
+// used for Azure Workload Identity. Both the pre-flight in apply_cluster.go and the
+// task-graph entry in the azuremodel must compute the identical string โ callers
+// should delegate here rather than re-deriving it inline.
+func (c *Cluster) AzureWorkloadIdentityName() string {
+ return "wi-" + strings.ReplaceAll(c.Name, ".", "-")
+}
+
func (c *Cluster) PublishesDNSRecords() bool {
if c.UsesNoneDNS() || dns.IsGossipClusterName(c.Name) {
return false
Cluster API Types & Version Conversion
pkg/apis/kops/componentconfig.go modified
@@ -958,6 +958,10 @@ type AzureSpec struct {
RouteTableName string `json:"routeTableName,omitempty"`
// AdminUser specifies the admin user of VMs.
AdminUser string `json:"adminUser,omitempty"`
+ // WorkloadIdentityClientID is the client ID of the User-Assigned Managed Identity
+ // used for Azure Workload Identity. This is populated automatically by kops
+ // when UseServiceAccountExternalPermissions is enabled.
+ WorkloadIdentityClientID string `json:"workloadIdentityClientID,omitempty"`
}
// CloudConfiguration defines the cloud provider configuration
Cluster API Types & Version Conversion
pkg/apis/kops/v1alpha2/componentconfig.go modified
@@ -964,6 +964,10 @@ type AzureSpec struct {
RouteTableName string `json:"routeTableName,omitempty"`
// AdminUser specifies the admin user of VMs.
AdminUser string `json:"adminUser,omitempty"`
+ // WorkloadIdentityClientID is the client ID of the User-Assigned Managed Identity
+ // used for Azure Workload Identity. This is populated automatically by kops
+ // when UseServiceAccountExternalPermissions is enabled.
+ WorkloadIdentityClientID string `json:"workloadIdentityClientID,omitempty"`
}
// CloudConfiguration defines the cloud provider configuration
Cluster API Types & Version Conversion
pkg/apis/kops/v1alpha2/zz_generated.conversion.go modified
@@ -1758,6 +1758,7 @@ func autoConvert_v1alpha2_AzureSpec_To_kops_AzureSpec(in *AzureSpec, out *kops.A
out.ResourceGroupName = in.ResourceGroupName
out.RouteTableName = in.RouteTableName
out.AdminUser = in.AdminUser
+ out.WorkloadIdentityClientID = in.WorkloadIdentityClientID
return nil
}
@@ -1773,6 +1774,7 @@ func autoConvert_kops_AzureSpec_To_v1alpha2_AzureSpec(in *kops.AzureSpec, out *A
out.ResourceGroupName = in.ResourceGroupName
out.RouteTableName = in.RouteTableName
out.AdminUser = in.AdminUser
+ out.WorkloadIdentityClientID = in.WorkloadIdentityClientID
return nil
}
Cluster API Types & Version Conversion
pkg/apis/kops/v1alpha3/componentconfig.go modified
@@ -955,6 +955,10 @@ type AzureSpec struct {
RouteTableName string `json:"routeTableName,omitempty"`
// AdminUser specifies the admin user of VMs.
AdminUser string `json:"adminUser,omitempty"`
+ // WorkloadIdentityClientID is the client ID of the User-Assigned Managed Identity
+ // used for Azure Workload Identity. This is populated automatically by kops
+ // when UseServiceAccountExternalPermissions is enabled.
+ WorkloadIdentityClientID string `json:"workloadIdentityClientID,omitempty"`
}
// CloudConfiguration defines the cloud provider configuration
Cluster API Types & Version Conversion
pkg/apis/kops/v1alpha3/zz_generated.conversion.go modified
@@ -1944,6 +1944,7 @@ func autoConvert_v1alpha3_AzureSpec_To_kops_AzureSpec(in *AzureSpec, out *kops.A
out.ResourceGroupName = in.ResourceGroupName
out.RouteTableName = in.RouteTableName
out.AdminUser = in.AdminUser
+ out.WorkloadIdentityClientID = in.WorkloadIdentityClientID
return nil
}
@@ -1959,6 +1960,7 @@ func autoConvert_kops_AzureSpec_To_v1alpha3_AzureSpec(in *kops.AzureSpec, out *A
out.ResourceGroupName = in.ResourceGroupName
out.RouteTableName = in.RouteTableName
out.AdminUser = in.AdminUser
+ out.WorkloadIdentityClientID = in.WorkloadIdentityClientID
return nil
}
Cluster API Types & Version Conversion โ Issues
-
medium
AzureWorkloadIdentityName() only replaces dots with hyphens, but Azure Managed Identity names must match the regex
^[a-zA-Z0-9-_]+$(max 128 chars). Other characters valid in a Kubernetes cluster FQDN โ such as underscores are already allowed, but if a cluster name ever contained characters outside that set (e.g. colons, slashes from unusual naming conventions), the produced identity name would be invalid. At minimum, a comment should document the assumed character set, or the function should validate/sanitise more broadly. -
medium
There are no unit tests for AzureWorkloadIdentityName() or AzureNetworkName(). Given that the explicit motivation for these helpers is consistency across call sites, a table-driven test covering edge cases (empty name, name with multiple dots, name that already has hyphens, shared-network NetworkID path for AzureNetworkName) would protect against future regressions.
-
low
The
WorkloadIdentityClientIDfield is documented as 'populated automatically by kops', but there is no validation that it is a well-formed Azure client ID (UUID format). If it is ever set incorrectly by a user or a bug, the error will surface far later during runtime. A validation webhook or admission-level check would make failures earlier and more actionable. -
low
The comment on AzureNetworkSecurityGroupName() now says 'callers relying on this invariant should prefer this helper over re-deriving the name inline', but the original comment stated the NSG 'shares its name with the virtual network' โ the updated doc no longer makes the naming relationship explicit, which could confuse readers unfamiliar with Azure's NSG/VNet co-naming convention.
Azure MSI SDK Vendor Dependency
Azure MSI SDK Vendor Dependency โ Key Signatures
| Name | File | What it does |
|---|---|---|
require github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/msi/armmsi v1.3.0 |
go.mod | Declares the armmsi v1.3.0 package as a direct dependency, enabling use of the Azure MSI resource manager client for managing user-assigned and system-assigned managed identities. |
Azure MSI SDK Vendor Dependency โ Walkthrough
This piece introduces the armmsi v1.3.0 library as a direct dependency in go.mod, placed alongside other Azure SDK resource manager packages in the require block, keeping the dependency list consistently organized.
The corresponding go.sum entries (both the module zip hash and the go.mod hash) are added, ensuring Go's module authentication mechanism can verify the integrity and authenticity of the downloaded package.
The vendor directory is also updated to include the vendored source of the library, which is consistent with the project's apparent use of go mod vendor โ meaning the code is checked into the repo and no network access is needed at build time.
The version v1.3.0 is a stable release of the armmsi SDK, so there are no pre-release or beta stability concerns unlike, for example, the armauthorization/v3 v3.0.0-beta.2 already present in the file.
This change is purely additive and infrastructural โ it does not alter any existing code paths, making the risk of regression very low.
Azure MSI SDK Vendor Dependency
go.mod modified
@@ -11,6 +11,7 @@ require (
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v3 v3.0.0-beta.2
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute v1.0.0
+ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/msi/armmsi v1.3.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork v1.1.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1
Azure MSI SDK Vendor Dependency
go.sum modified
@@ -28,6 +28,8 @@ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.0 h1:2qsI
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.0/go.mod h1:AW8VEadnhw9xox+VaVd9sP7NjzOAnaZBLRH6Tq3cJ38=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/managementgroups/armmanagementgroups v1.0.0 h1:pPvTJ1dY0sA35JOeFq6TsY2xj6Z85Yo23Pj4wCCvu4o=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/managementgroups/armmanagementgroups v1.0.0/go.mod h1:mLfWfj8v3jfWKsL9G4eoBoXVcsqcIUTapmdKy7uGOp0=
+github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/msi/armmsi v1.3.0 h1:L7G3dExHBgUxsO3qpTGhk/P2dgnYyW48yn7AO33Tbek=
+github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/msi/armmsi v1.3.0/go.mod h1:Ms6gYEy0+A2knfKrwdatsggTXYA2+ICKug8w7STorFw=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork v1.1.0 h1:QM6sE5k2ZT/vI5BEe0r7mqjsUSnhVBFbOsVkEuaEfiA=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork v1.1.0/go.mod h1:243D9iHbcQXoFUtgHJwL7gl2zx1aDuDMjvBZVGr2uW0=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 h1:Dd+RhdJn0OTtVGaeDLZpcumkIVCtA/3/Fo42+eoYvVM=
Azure MSI SDK Vendor Dependency โ Issues
-
low
Other Azure resource manager dependencies in go.mod use notably older versions (e.g., armcompute v1.0.0, armnetwork v1.1.0). While not a bug, using armmsi v1.3.0 alongside much older sibling packages may create minor inconsistency, and it's worth verifying all packages share compatible versions of transitive dependencies (especially
azure-sdk-for-go/sdk/azcore). -
low
The diff does not show the vendor directory contents explicitly, but if
go mod vendorwas not re-run after adding this dependency, the vendored files may be incomplete or missing. The reviewer should confirm thatvendor/github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/msi/armmsiis fully present and thatvendor/modules.txthas been updated accordingly.
Azure Infrastructure Tasks (ManagedIdentity, FederatedIdentityCredential, RoleAssignment)
fi.Task implementations in upup/pkg/fi/cloudup/azuretasks: ManagedIdentity (creates/updates a User-Assigned Managed Identity via armmsi), FederatedIdentityCredential (attaches OIDC federation bindings to the UAMI), and an updated RoleAssignment that can accept a ManagedIdentity reference as the principal instead of a raw principal ID. These are the core idempotent building blocks for the Workload Identity feature.Azure Infrastructure Tasks (ManagedIdentity, FederatedIdentityCredential, RoleAssignment) โ Diagram
Azure Infrastructure Tasks (ManagedIdentity, FederatedIdentityCredential, RoleAssignment) โ Key Signatures
| Name | File | What it does |
|---|---|---|
ManagedIdentity |
upup/pkg/fi/cloudup/azuretasks/managedidentity.go | New fi.Task struct representing an Azure User-Assigned Managed Identity, with Find/CheckChanges/RenderAzure implementing idempotent create-or-update via armmsi, and post-creation population of ClientID and PrincipalID fields for use by dependent tasks. |
ManagedIdentity.Find |
upup/pkg/fi/cloudup/azuretasks/managedidentity.go | Fetches the existing UAMI from Azure and, critically, also populates ClientID and PrincipalID on the expected task (m) so downstream tasks like RoleAssignment can read them even when no update is needed. |
ManagedIdentity.RenderAzure |
upup/pkg/fi/cloudup/azuretasks/managedidentity.go | Calls armmsi CreateOrUpdate and stores the server-returned ClientID and PrincipalID back onto the expected task struct e for use by dependents in the same execution graph. |
FederatedIdentityCredential |
upup/pkg/fi/cloudup/azuretasks/federatedidentitycredential.go | New fi.Task struct that attaches an OIDC federated identity credential (issuer, subject, audiences) to a UAMI, enabling Kubernetes service accounts to impersonate the identity without a client secret. |
FederatedIdentityCredential.Find |
upup/pkg/fi/cloudup/azuretasks/federatedidentitycredential.go | Queries the Azure MSI API for an existing federated credential by resource group, UAMI name, and credential name, returning nil on 404 to signal a needed create. |
FederatedIdentityCredential.RenderAzure |
upup/pkg/fi/cloudup/azuretasks/federatedidentitycredential.go | Calls armmsi FederatedIdentityCredential CreateOrUpdate with the desired issuer, subject, and audiences to create or reconcile the OIDC binding. |
RoleAssignment.principalID |
upup/pkg/fi/cloudup/azuretasks/roleassignment.go | New helper that returns the principal ID from whichever source is set (ManagedIdentity or VMScaleSet), abstracting the two principal types away from Find and createNewRoleAssignment. |
RoleAssignment.Find |
upup/pkg/fi/cloudup/azuretasks/roleassignment.go | Updated to use principalID() instead of directly dereferencing VMScaleSet, and to conditionally skip the expensive VMSS list query when the principal is a ManagedIdentity. |
Azure Infrastructure Tasks (ManagedIdentity, FederatedIdentityCredential, RoleAssignment) โ Walkthrough
The ManagedIdentity task follows the standard kops fi.Task pattern: Find queries Azure and returns an existing-state object, CheckChanges validates immutable fields, and RenderAzure performs the actual create-or-update.
A key design choice in ManagedIdentity.Find is that it writes ClientID and PrincipalID back onto the expected task (m) in addition to the returned actual object โ this mirrors the VMScaleSet.Find convention and ensures dependent tasks (RoleAssignment, cloud config generation) can read these server-generated IDs even when the UAMI hasn't changed and RenderAzure is never called.
FederatedIdentityCredential holds a pointer to ManagedIdentity and uses its name (not ID) as an API key โ the armmsi API scopes federated credentials by resource group + UAMI name + credential name, so the task naturally expresses this three-part ownership.
The RoleAssignment refactor introduces principalID() to cleanly abstract over two possible principal sources, removing the hard dependency on VMScaleSet.PrincipalID and making the Find early-exit logic (nil PID โ skip) work for both cases.
The VMSS list-and-search logic in RoleAssignment.Find is now gated behind r.VMScaleSet != nil, so ManagedIdentity-backed role assignments avoid an unnecessary and potentially expensive list API call.
In createNewRoleAssignment, e.VMScaleSet.PrincipalID is replaced with e.principalID(), completing the abstraction so the same assignment creation path serves both principal types without duplication.
Azure Infrastructure Tasks (ManagedIdentity, FederatedIdentityCredential, RoleAssignment)
upup/pkg/fi/cloudup/azuretasks/federatedidentitycredential.go added
@@ -0,0 +1,130 @@
+/*
+Copyright 2026 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package azuretasks
+
+import (
+ "context"
+ "errors"
+ "fmt"
+
+ "github.com/Azure/azure-sdk-for-go/sdk/azcore"
+ "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/msi/armmsi"
+ "k8s.io/klog/v2"
+ "k8s.io/kops/upup/pkg/fi"
+ "k8s.io/kops/upup/pkg/fi/cloudup/azure"
+)
+
+// FederatedIdentityCredential links a Kubernetes service account to an Azure User-Assigned Managed Identity.
+// +kops:fitask
+type FederatedIdentityCredential struct {
+ Name *string
+ Lifecycle fi.Lifecycle
+
+ ManagedIdentity *ManagedIdentity
+ ResourceGroup *ResourceGroup
+ Issuer *string
+ Subject *string
+ Audiences []*string
+}
+
+var (
+ _ fi.CloudupTask = &FederatedIdentityCredential{}
+ _ fi.CompareWithID = &FederatedIdentityCredential{}
+)
+
+// CompareWithID returns the Name.
+func (f *FederatedIdentityCredential) CompareWithID() *string {
+ return f.Name
+}
+
+// Find discovers the FederatedIdentityCredential in the cloud provider.
+func (f *FederatedIdentityCredential) Find(c *fi.CloudupContext) (*FederatedIdentityCredential, error) {
+ cloud := c.T.Cloud.(azure.AzureCloud)
+ found, err := cloud.FederatedIdentityCredential().Get(
+ context.TODO(),
+ fi.ValueOf(f.ResourceGroup.Name),
+ fi.ValueOf(f.ManagedIdentity.Name),
+ fi.ValueOf(f.Name),
+ )
+ if err != nil {
+ var respErr *azcore.ResponseError
+ if ok := errors.As(err, &respErr); ok && respErr.StatusCode == 404 {
+ return nil, nil
+ }
+ return nil, fmt.Errorf("getting federated identity credential %q: %w", fi.ValueOf(f.Name), err)
+ }
+
+ result := &FederatedIdentityCredential{
+ Name: f.Name,
+ Lifecycle: f.Lifecycle,
+ ManagedIdentity: f.ManagedIdentity,
+ ResourceGroup: f.ResourceGroup,
+ }
+ if found.Properties != nil {
+ result.Issuer = found.Properties.Issuer
+ result.Subject = found.Properties.Subject
+ result.Audiences = found.Properties.Audiences
+ }
+ return result, nil
+}
+
+// Run implements fi.Task.Run.
+func (f *FederatedIdentityCredential) Run(c *fi.CloudupContext) error {
+ return fi.CloudupDefaultDeltaRunMethod(f, c)
+}
+
+// CheckChanges returns an error if a change is not allowed.
+func (*FederatedIdentityCredential) CheckChanges(a, e, changes *FederatedIdentityCredential) error {
+ if a == nil {
+ if e.Name == nil {
+ return fi.RequiredField("Name")
+ }
+ return nil
+ }
+ if changes.Name != nil {
+ return fi.CannotChangeField("Name")
+ }
+ return nil
+}
+
+// RenderAzure creates or updates a federated identity credential.
+func (*FederatedIdentityCredential) RenderAzure(t *azure.AzureAPITarget, a, e, changes *FederatedIdentityCredential) error {
+ if a == nil {
+ klog.Infof("Creating a new Federated Identity Credential with name: %s", fi.ValueOf(e.Name))
+ } else {
+ klog.Infof("Updating a Federated Identity Credential with name: %s", fi.ValueOf(e.Name))
+ }
+
+ _, err := t.Cloud.FederatedIdentityCredential().CreateOrUpdate(
+ context.TODO(),
+ fi.ValueOf(e.ResourceGroup.Name),
+ fi.ValueOf(e.ManagedIdentity.Name),
+ fi.ValueOf(e.Name),
+ armmsi.FederatedIdentityCredential{
+ Properties: &armmsi.FederatedIdentityCredentialProperties{
+ Issuer: e.Issuer,
+ Subject: e.Subject,
+ Audiences: e.Audiences,
+ },
+ },
+ )
+ if err != nil {
+ return fmt.Errorf("creating/updating federated identity credential: %w", err)
+ }
+
+ return nil
+}
Azure Infrastructure Tasks (ManagedIdentity, FederatedIdentityCredential, RoleAssignment)
upup/pkg/fi/cloudup/azuretasks/managedidentity.go added
@@ -0,0 +1,135 @@
+/*
+Copyright 2026 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package azuretasks
+
+import (
+ "context"
+ "errors"
+ "fmt"
+
+ "github.com/Azure/azure-sdk-for-go/sdk/azcore"
+ "github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
+ "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/msi/armmsi"
+ "k8s.io/klog/v2"
+ "k8s.io/kops/upup/pkg/fi"
+ "k8s.io/kops/upup/pkg/fi/cloudup/azure"
+)
+
+// ManagedIdentity is an Azure User-Assigned Managed Identity.
+// +kops:fitask
+type ManagedIdentity struct {
+ Name *string
+ Lifecycle fi.Lifecycle
+
+ ResourceGroup *ResourceGroup
+ Tags map[string]*string
+
+ // ClientID is populated after creation โ this is the UAMI's client ID
+ // needed for workload identity azure.json configuration.
+ ClientID *string
+ // PrincipalID is populated after creation โ this is the UAMI's principal ID
+ // needed for role assignments.
+ PrincipalID *string
+}
+
+var (
+ _ fi.CloudupTask = &ManagedIdentity{}
+ _ fi.CompareWithID = &ManagedIdentity{}
+)
+
+// CompareWithID returns the Name of the managed identity.
+func (m *ManagedIdentity) CompareWithID() *string {
+ return m.Name
+}
+
+// Find discovers the ManagedIdentity in the cloud provider.
+func (m *ManagedIdentity) Find(c *fi.CloudupContext) (*ManagedIdentity, error) {
+ cloud := c.T.Cloud.(azure.AzureCloud)
+ found, err := cloud.ManagedIdentity().Get(context.TODO(), fi.ValueOf(m.ResourceGroup.Name), fi.ValueOf(m.Name))
+ if err != nil {
+ var respErr *azcore.ResponseError
+ if ok := errors.As(err, &respErr); ok && respErr.StatusCode == 404 {
+ return nil, nil
+ }
+ return nil, fmt.Errorf("getting managed identity %q: %w", fi.ValueOf(m.Name), err)
+ }
+
+ result := &ManagedIdentity{
+ Name: m.Name,
+ Lifecycle: m.Lifecycle,
+ ResourceGroup: m.ResourceGroup,
+ Tags: found.Tags,
+ }
+ if found.Properties != nil {
+ result.ClientID = found.Properties.ClientID
+ result.PrincipalID = found.Properties.PrincipalID
+ // Also populate the expected task so dependent tasks (e.g. RoleAssignment)
+ // can read these server-generated IDs when RenderAzure is skipped because
+ // no changes are needed. Matches the VMScaleSet.Find convention.
+ m.ClientID = found.Properties.ClientID
+ m.PrincipalID = found.Properties.PrincipalID
+ }
+ return result, nil
+}
+
+// Run implements fi.Task.Run.
+func (m *ManagedIdentity) Run(c *fi.CloudupContext) error {
+ return fi.CloudupDefaultDeltaRunMethod(m, c)
+}
+
+// CheckChanges returns an error if a change is not allowed.
+func (*ManagedIdentity) CheckChanges(a, e, changes *ManagedIdentity) error {
+ if a == nil {
+ if e.Name == nil {
+ return fi.RequiredField("Name")
+ }
+ return nil
+ }
+ if changes.Name != nil {
+ return fi.CannotChangeField("Name")
+ }
+ return nil
+}
+
+// RenderAzure creates or updates a managed identity.
+func (*ManagedIdentity) RenderAzure(t *azure.AzureAPITarget, a, e, changes *ManagedIdentity) error {
+ if a == nil {
+ klog.Infof("Creating a new Managed Identity with name: %s", fi.ValueOf(e.Name))
+ } else {
+ klog.Infof("Updating a Managed Identity with name: %s", fi.ValueOf(e.Name))
+ }
+
+ result, err := t.Cloud.ManagedIdentity().CreateOrUpdate(
+ context.TODO(),
+ fi.ValueOf(e.ResourceGroup.Name),
+ fi.ValueOf(e.Name),
+ armmsi.Identity{
+ Location: to.Ptr(t.Cloud.Region()),
+ Tags: e.Tags,
+ },
+ )
+ if err != nil {
+ return fmt.Errorf("creating/updating managed identity: %w", err)
+ }
+
+ if result.Properties != nil {
+ e.ClientID = result.Properties.ClientID
+ e.PrincipalID = result.Properties.PrincipalID
+ }
+
+ return nil
+}
Azure Infrastructure Tasks (ManagedIdentity, FederatedIdentityCredential, RoleAssignment)
upup/pkg/fi/cloudup/azuretasks/roleassignment.go modified
@@ -45,8 +45,11 @@ type RoleAssignment struct {
Scope *string
VMScaleSet *VMScaleSet
- ID *string
- RoleDefID *string
+ // ManagedIdentity is an alternative principal source to VMScaleSet.
+ // Exactly one of VMScaleSet or ManagedIdentity should be set.
+ ManagedIdentity *ManagedIdentity
+ ID *string
+ RoleDefID *string
}
var (
@@ -60,12 +63,22 @@ func (r *RoleAssignment) CompareWithID() *string {
return r.Name
}
+// principalID returns the principal ID from either VMScaleSet or ManagedIdentity.
+func (r *RoleAssignment) principalID() *string {
+ if r.ManagedIdentity != nil && r.ManagedIdentity.PrincipalID != nil {
+ return r.ManagedIdentity.PrincipalID
+ }
+ if r.VMScaleSet != nil && r.VMScaleSet.PrincipalID != nil {
+ return r.VMScaleSet.PrincipalID
+ }
+ return nil
+}
+
// Find discovers the RoleAssignment in the cloud provider.
func (r *RoleAssignment) Find(c *fi.CloudupContext) (*RoleAssignment, error) {
- if r.VMScaleSet.PrincipalID == nil {
- // PrincipalID of the VM Scale Set hasn't yet been
- // populated. No corresponding Role Assignment
- // shouldn't exist in Cloud.
+ pid := r.principalID()
+ if pid == nil {
+ // PrincipalID hasn't yet been populated.
return nil, nil
}
@@ -75,15 +88,13 @@ func (r *RoleAssignment) Find(c *fi.CloudupContext) (*RoleAssignment, error) {
return nil, err
}
- principalID := *r.VMScaleSet.PrincipalID
+ principalID := *pid
var found *authz.RoleAssignment
for i := range rs {
ra := rs[i]
if ra.Properties == nil {
continue
}
- // Use a name constructed by VMSS and Role definition ID to find a Role Assignment. We cannot use ra.Name
- // as it is set to a randomly generated GUID.
l := strings.Split(*ra.Properties.RoleDefinitionID, "/")
roleDefID := l[len(l)-1]
if *ra.Properties.PrincipalID == principalID && roleDefID == *r.RoleDefID {
@@ -95,36 +106,45 @@ func (r *RoleAssignment) Find(c *fi.CloudupContext) (*RoleAssignment, error) {
return nil, nil
}
- // Query VM Scale Sets and find one that has matching Principal ID.
- vs, err := cloud.VMScaleSet().List(context.TODO(), *r.VMScaleSet.ResourceGroup.Name)
- if err != nil {
- return nil, err
+ result := &RoleAssignment{
+ Name: r.Name,
+ Lifecycle: r.Lifecycle,
+ Scope: found.Properties.Scope,
+ ID: found.ID,
+ RoleDefID: to.Ptr(filepath.Base(*found.Properties.RoleDefinitionID)),
}
- var foundVMSS *compute.VirtualMachineScaleSet
- for _, v := range vs {
- if v.Identity == nil {
- continue
+
+ // Populate the original principal source reference.
+ if r.ManagedIdentity != nil {
+ result.ManagedIdentity = &ManagedIdentity{
+ Name: r.ManagedIdentity.Name,
}
- if *v.Identity.PrincipalID == principalID {
- foundVMSS = v
- break
+ } else if r.VMScaleSet != nil {
+ // Query VM Scale Sets and find one that has matching Principal ID.
+ vs, err := cloud.VMScaleSet().List(context.TODO(), *r.VMScaleSet.ResourceGroup.Name)
+ if err != nil {
+ return nil, err
+ }
+ var foundVMSS *compute.VirtualMachineScaleSet
+ for _, v := range vs {
+ if v.Identity == nil {
+ continue
+ }
+ if *v.Identity.PrincipalID == principalID {
+ foundVMSS = v
+ break
+ }
+ }
+ if foundVMSS == nil {
+ return nil, fmt.Errorf("corresponding VM Scale Set not found for Role Assignment: %s", *found.ID)
+ }
+ result.VMScaleSet = &VMScaleSet{
+ Name: foundVMSS.Name,
}
- }
- if foundVMSS == nil {
- return nil, fmt.Errorf("corresponding VM Scale Set not found for Role Assignment: %s", *found.ID)
}
r.ID = found.ID
- return &RoleAssignment{
- Name: r.Name,
- Lifecycle: r.Lifecycle,
- Scope: found.Properties.Scope,
- VMScaleSet: &VMScaleSet{
- Name: foundVMSS.Name,
- },
- ID: found.ID,
- RoleDefID: to.Ptr(filepath.Base(*found.Properties.RoleDefinitionID)),
- }, nil
+ return result, nil
}
// Run implements fi.Task.Run.
@@ -169,7 +189,7 @@ func createNewRoleAssignment(t *azure.AzureAPITarget, e *RoleAssignment) error {
roleAssignment := authz.RoleAssignmentCreateParameters{
Properties: &authz.RoleAssignmentProperties{
RoleDefinitionID: to.Ptr(roleDefID),
- PrincipalID: e.VMScaleSet.PrincipalID,
+ PrincipalID: e.principalID(),
},
}
ra, err := t.Cloud.RoleAssignment().Create(context.TODO(), scope, roleAssignmentName, roleAssignment)
Azure Infrastructure Tasks (ManagedIdentity, FederatedIdentityCredential, RoleAssignment) โ Issues
-
medium
ManagedIdentity.RenderAzure only populates e.ClientID and e.PrincipalID when the identity is created/updated, but on a no-op run (no changes detected), RenderAzure is never called. The workaround of also setting m.ClientID/m.PrincipalID in Find covers the no-op path, but if Find is not called before RenderAzure (e.g. on first apply when Find returns nil and then RenderAzure is called), dependent tasks that read e.ManagedIdentity.PrincipalID may race or see nil. The code does set it via RenderAzure result in that case, but only on
e, so tasks that hold a different pointer to ManagedIdentity may still see nil. This pointer aliasing should be explicitly documented or made structurally safe. -
medium
RoleAssignment.principalID() silently returns nil if both VMScaleSet and ManagedIdentity are set but neither has a PrincipalID populated yet โ the nil return causes Find to bail early (correct), but createNewRoleAssignment would pass nil as PrincipalID to the Azure API, resulting in a confusing API error rather than a clear kops error. A nil check with an explicit error return in createNewRoleAssignment would be safer.
-
low
There is no validation anywhere that exactly one of VMScaleSet or ManagedIdentity is set on RoleAssignment. The comment says 'exactly one should be set', but CheckChanges (not shown in the diff) or a new validation step should enforce this to prevent misconfiguration.
-
low
The copyright year in both new files reads '2026', which is in the future. This should be '2024' or '2025' per the actual authorship year.
-
low
FederatedIdentityCredential.Find copies ManagedIdentity and ResourceGroup references from the expected task but does not deep-copy them โ if the caller modifies those structs later the returned actual object would also be affected. This is consistent with the rest of the codebase but worth noting for correctness.
-
low
No unit tests are included for ManagedIdentity or FederatedIdentityCredential. Given that Find has non-trivial side-effect logic (mutating m in place), at minimum a table-driven test covering the 404 path, the found-no-properties path, and the found-with-properties path would reduce regression risk.
Azure Blob VFS Enhancements (OIDC Discovery Store)
util/pkg/vfs/azureblob.go) with the GetHTTPsUrl() and IsBucketPublic() methods needed to use Azure Blob Storage as an OIDC discovery document store. Updates validation (pkg/apis/kops/validation/legacy.go) to accept AzureBlobPath as a valid discoveryStore VFS type, and updates the discovery options builder (pkg/model/components/discovery.go) to derive the service account issuer URL from an Azure Blob path.Azure Blob VFS Enhancements (OIDC Discovery Store) โ Key Signatures
| Name | File | What it does |
|---|---|---|
AzureBlobPath.GetHTTPsUrl() |
util/pkg/vfs/azureblob.go | Constructs and returns the public HTTPS URL for the blob by reading the storage account name from the AZURE_STORAGE_ACCOUNT environment variable and combining it with the container and key fields. |
AzureBlobPath.IsBucketPublic(ctx context.Context) |
util/pkg/vfs/azureblob.go | Checks whether the Azure Blob container permits anonymous reads by fetching the container's properties and treating any non-nil BlobPublicAccess value as public access enabled. |
validateServiceAccountIssuerDiscovery (modified) |
pkg/apis/kops/validation/legacy.go | Adds a case for *vfs.AzureBlobPath to the discovery store VFS type switch, allowing Azure Blob paths to pass validation without an error. |
DiscoveryOptionsBuilder.BuildOptions (modified) |
pkg/model/components/discovery.go | Adds a case for *vfs.AzureBlobPath that calls GetHTTPsUrl() to derive the OIDC service account issuer URL from the Azure Blob path. |
Azure Blob VFS Enhancements (OIDC Discovery Store) โ Walkthrough
The core addition is two new methods on AzureBlobPath in azureblob.go. GetHTTPsUrl() assembles the canonical blob HTTPS URL using the well-known Azure Blob endpoint pattern (<account>.blob.core.windows.net/<container>/<key>), relying on the AZURE_STORAGE_ACCOUNT environment variable for the account name โ a piece of information not stored on the path struct itself.
IsBucketPublic() uses the existing getClient() helper to obtain an Azure SDK client, then calls GetProperties on the container. The check is simple: Azure SDK represents a private container by leaving BlobPublicAccess nil, so any non-nil pointer value means public access is enabled at some level (blob or container). Importantly, the comment explicitly states that kops does not manage the ACL itself โ callers must pre-configure the container.
In validation/legacy.go, the new case *vfs.AzureBlobPath is inserted into the existing type switch, which is the minimum change needed to stop rejecting Azure Blob paths. The error message on the default branch is also updated to accurately reflect which VFS types are now supported.
In discovery.go, the BuildOptions method's type switch gains the analogous case *vfs.AzureBlobPath that calls base.GetHTTPsUrl() and assigns the result to serviceAccountIssuer, exactly mirroring the GCS path handling pattern already in place.
The three changes together form a complete, coherent feature: the VFS layer knows how to produce the public URL and check container publicity, validation accepts the new type, and the options builder uses the URL as the OIDC issuer.
Azure Blob VFS Enhancements (OIDC Discovery Store)
pkg/apis/kops/validation/legacy.go modified
@@ -238,14 +238,16 @@ func validateServiceAccountIssuerDiscovery(c *kops.Cluster, said *kops.ServiceAc
}
case *vfs.GSPath:
// No known restrictions currently. Added here to avoid falling into the default catch all below.
+ case *vfs.AzureBlobPath:
+ // Azure Blob Storage is supported as a discovery store.
case *vfs.MemFSPath:
// memfs is ok for tests; not OK otherwise
if !base.IsClusterReadable() {
// (If this _is_ a test, we should call MarkClusterReadable)
allErrs = append(allErrs, field.Invalid(saidStoreField, discoveryStore, "S3 is the only supported VFS for discoveryStore"))
}
default:
- allErrs = append(allErrs, field.Invalid(saidStoreField, discoveryStore, "S3 is the only supported VFS for discoveryStore"))
+ allErrs = append(allErrs, field.Invalid(saidStoreField, discoveryStore, "S3, GCS, and Azure Blob are the only supported VFS types for discoveryStore"))
}
}
}
Azure Blob VFS Enhancements (OIDC Discovery Store)
pkg/model/components/discovery.go modified
@@ -66,6 +66,11 @@ func (b *DiscoveryOptionsBuilder) BuildOptions(o *kops.Cluster) error {
if err != nil {
return err
}
+ case *vfs.AzureBlobPath:
+ serviceAccountIssuer, err = base.GetHTTPsUrl()
+ if err != nil {
+ return err
+ }
case *vfs.MemFSPath:
if !base.IsClusterReadable() {
// If this _is_ a test, we should call MarkClusterReadable
Azure Blob VFS Enhancements (OIDC Discovery Store)
util/pkg/vfs/azureblob.go modified
@@ -89,6 +89,35 @@ func (p *AzureBlobPath) Path() string {
return fmt.Sprintf("azureblob://%s/%s", p.container, p.key)
}
+// GetHTTPsUrl returns the public HTTPS URL for the Azure Blob path.
+// The storage account name is read from the AZURE_STORAGE_ACCOUNT environment variable.
+func (p *AzureBlobPath) GetHTTPsUrl() (string, error) {
+ storageAccountName := os.Getenv("AZURE_STORAGE_ACCOUNT")
+ if storageAccountName == "" {
+ return "", fmt.Errorf("AZURE_STORAGE_ACCOUNT is not set")
+ }
+ url := fmt.Sprintf("https://%s.blob.core.windows.net/%s/%s", storageAccountName, p.container, p.key)
+ return strings.TrimSuffix(url, "/"), nil
+}
+
+// IsBucketPublic reports whether the Azure Blob container allows anonymous
+// read access (either "blob" or "container" public access level). OIDC
+// discovery endpoints require this; kops does not manage container ACLs
+// itself, so callers that need public access must pre-configure the container.
+func (p *AzureBlobPath) IsBucketPublic(ctx context.Context) (bool, error) {
+ client, err := p.getClient(ctx)
+ if err != nil {
+ return false, err
+ }
+ resp, err := client.ServiceClient().NewContainerClient(p.container).GetProperties(ctx, nil)
+ if err != nil {
+ return false, fmt.Errorf("getting properties for Azure Blob container %q: %w", p.container, err)
+ }
+ // BlobPublicAccess is nil for "private"; any non-nil value ("blob" or
+ // "container") permits anonymous reads of individual blobs.
+ return resp.BlobPublicAccess != nil, nil
+}
+
// Join returns a new path that joins the current path and given relative paths.
func (p *AzureBlobPath) Join(relativePath ...string) Path {
args := []string{p.key}
Azure Blob VFS Enhancements (OIDC Discovery Store) โ Issues
-
high
GetHTTPsUrl() reads AZURE_STORAGE_ACCOUNT from the environment at call time, but the storage account name is architecture-critical and environment-dependent. If the environment variable is unset (e.g., in a controller pod or CI job), the issuer URL silently changes or returns an error, whereas other cloud VFS paths (S3, GCS) derive the URL from the path struct fields alone. This creates an operational footgun where the same AzureBlobPath value produces different issuer URLs in different environments. Consider storing the account name on AzureBlobPath during construction (where the env var is already read) and using it here.
-
medium
IsBucketPublic() is implemented and plumbed correctly, but it is never actually called during the OIDC discovery setup flow. Neither the validation path nor BuildOptions invokes it to warn or error if the container is private. Other cloud backends (S3, GCS) appear to have similar gaps, but adding IsBucketPublic without wiring it into a meaningful check means the method provides no real protection against misconfiguration.
-
medium
The URL produced by GetHTTPsUrl() uses the hard-coded Azure public cloud endpoint (core.windows.net). This will be wrong for Azure Government (core.usgovcloudapi.net), Azure China (core.chinacloudapi.cn), or custom Azure Stack endpoints. S3 paths handle alternate endpoints differently; consider making the endpoint suffix configurable or deriving it from the Azure environment SDK.
-
low
The TrimSuffix(url, "/") call in GetHTTPsUrl() is defensive but unnecessary: since p.key is already part of fmt.Sprintf and neither the container nor key fields should carry a trailing slash when the struct is properly constructed, this trim will silently mask a key that ends in '/' โ potentially producing a URL that doesn't correspond to an actual blob object.
-
low
The validation error message now reads "S3, GCS, and Azure Blob are the only supported VFS types for discoveryStore", but the MemFSPath branch still shows the old message ("S3 is the only supported VFS for discoveryStore") when the cluster is not readable. That old message is now stale and misleading.
OIDC Issuer Discovery Publishing for Azure Blob
pkg/model/issuerdiscovery.go to handle *vfs.AzureBlobPath as a discovery store target: it checks that the container has public blob access (required for AAD token exchange) and publishes the OIDC discovery document and JWKS to Azure Blob Storage, mirroring the existing S3 and GCS paths.OIDC Issuer Discovery Publishing for Azure Blob โ Diagram
OIDC Issuer Discovery Publishing for Azure Blob โ Key Signatures
| Name | File | What it does |
|---|---|---|
IssuerDiscoveryModelBuilder.Build |
pkg/model/issuerdiscovery.go | Builds the OIDC issuer discovery model by switching on the VFS path type of the discovery store and publishing OIDC discovery documents with appropriate access controls for each backend (S3, GCS, AzureBlob, MemFS). |
OIDC Issuer Discovery Publishing for Azure Blob โ Walkthrough
The Build method uses a type switch on the discoveryStore VFS path to handle each storage backend differently.
For Azure Blob, unlike S3 or GCS which support per-object ACLs, Azure enforces access at the container level (Private / Blob / Container modes), so kops cannot programmatically set a public ACL on individual blobs after upload.
The new case *vfs.AzureBlobPath block calls discoveryStore.IsBucketPublic(ctx) to check whether the container was pre-configured for anonymous "Blob" access by the user.
If the container is not public, Build returns a descriptive error immediately, preventing silent failures where OIDC discovery documents would be uploaded but be inaccessible to AAD token exchange.
If the container is public, the code falls through naturally to whatever downstream publishing logic already exists (shared after the switch), mirroring the S3 and GCS cases without duplicating upload logic.
OIDC Issuer Discovery Publishing for Azure Blob
pkg/model/issuerdiscovery.go modified
@@ -123,6 +123,20 @@ func (b *IssuerDiscoveryModelBuilder) Build(c *fi.CloudupModelBuilderContext) er
klog.Infof("using user managed serviceAccountIssuers")
}
+ case *vfs.AzureBlobPath:
+ // Azure Blob Storage uses container-level public access (Private / Blob /
+ // Container), not per-object ACLs like S3 or GCS, so kops cannot flip a
+ // PublicACL on the individual discovery files. The user must pre-configure
+ // the container with anonymous "Blob" access before running kops; otherwise
+ // the OIDC discovery endpoint is unreachable and AAD token exchange fails.
+ isPublic, err := discoveryStore.IsBucketPublic(ctx)
+ if err != nil {
+ return fmt.Errorf("checking if Azure Blob container was public: %w", err)
+ }
+ if !isPublic {
+ return fmt.Errorf("serviceAccountIssuers Azure Blob storage %q is not public", discoveryStore.Path())
+ }
+
case *vfs.MemFSPath:
// ok
OIDC Issuer Discovery Publishing for Azure Blob โ Issues
-
medium
The new
case *vfs.AzureBlobPathblock only validates public access but does not explicitly publish the OIDC discovery document or JWKS โ it relies on shared post-switch logic. If that shared logic does not handle AzureBlobPath correctly (e.g., it tries to set per-object ACLs unsupported by Azure), the upload could fail silently or with a confusing error. The diff does not show the surrounding context, so this risk cannot be fully assessed. -
medium
There is no test coverage visible in this diff for the new Azure Blob case โ neither a unit test mocking
IsBucketPublicreturning false (error path) nor returning true (happy path). Given the critical nature of OIDC misconfiguration, both branches should be tested. -
low
The error message 'serviceAccountIssuers Azure Blob storage %q is not public' uses
discoveryStore.Path()for the %q argument. It would be more actionable to include remediation guidance (e.g., 'set container access level to Blob or Container') directly in the error string, as misconfigured Azure access is a common operator mistake. -
low
The comment block inside the case is quite long (7 lines). While informative, some of this context belongs in documentation or a runbook rather than inline code comments, which can become stale.
WorkloadIdentity Model Builder
pkg/model/azuremodel/workloadidentity.go, a new fi.CloudupModelBuilder that, when UseServiceAccountExternalPermissions is enabled, wires together the three Azure tasks: creates the UAMI, assigns the Contributor role on the resource group scope, and registers federated identity credentials for the cloud-controller-manager and CSI azuredisk service accounts.WorkloadIdentity Model Builder โ Diagram
WorkloadIdentity Model Builder โ Key Signatures
| Name | File | What it does |
|---|---|---|
WorkloadIdentityModelBuilder.Build |
pkg/model/azuremodel/workloadidentity.go | Orchestrates creation of a UAMI, a Contributor role assignment scoped to the resource group, and two federated identity credentials (CCM and CSI azuredisk) by adding tasks to the model builder context. |
WorkloadIdentity Model Builder โ Walkthrough
The Build method is guarded by UseServiceAccountExternalPermissions(), so it short-circuits and is a no-op for clusters that do not use OIDC-based workload identity โ this is the correct escape hatch.
It then validates that ServiceAccountIssuer is non-empty, since the issuer URL is the OIDC endpoint that Azure AD must trust for token exchange; without it the federated credentials would be meaningless.
The UAMI task is added first and stored in a local variable (uami), which is then referenced directly by both the RoleAssignment and FederatedIdentityCredential tasks โ this establishes implicit task ordering in the kops dependency graph without explicit GetDependencies wiring.
The Contributor role is assigned at the resource-group scope using a hardcoded well-known Azure built-in role definition GUID (b24988ac-6180-42a0-ab88-20f7382dd24c), and the role assignment name is hardcoded to "wi-uami-contributor", which will collide if multiple clusters share the same resource group.
Two federated identity credentials are registered โ one for cloud-controller-manager and one for csi-azuredisk-controller-sa, both in kube-system โ using the standard Kubernetes service account subject format (system:serviceaccount:<ns>:<sa>).
The audience is hardcoded to api://AzureADTokenExchange, which is the correct and expected value for Azure AD workload identity federation, so this is intentional and correct.
Notably, there is no federated identity credential for other Azure-dependent components (e.g., external-dns, cluster-autoscaler), which may be a conscious MVP scope decision but could surface as a gap for adopters.
WorkloadIdentity Model Builder
pkg/model/azuremodel/workloadidentity.go added
@@ -0,0 +1,98 @@
+/*
+Copyright 2026 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package azuremodel
+
+import (
+ "fmt"
+
+ "k8s.io/kops/upup/pkg/fi"
+ "k8s.io/kops/upup/pkg/fi/cloudup/azuretasks"
+)
+
+// Azure built-in role definition IDs.
+// See: https://learn.microsoft.com/azure/role-based-access-control/built-in-roles
+const (
+ // azureContributorRoleDefID is the ID of the built-in "Contributor" role.
+ azureContributorRoleDefID = "b24988ac-6180-42a0-ab88-20f7382dd24c"
+)
+
+// WorkloadIdentityModelBuilder configures Azure Workload Identity resources
+// (UAMI, federated identity credentials, and role assignments).
+type WorkloadIdentityModelBuilder struct {
+ *AzureModelContext
+ Lifecycle fi.Lifecycle
+}
+
+var _ fi.CloudupModelBuilder = &WorkloadIdentityModelBuilder{}
+
+func (b *WorkloadIdentityModelBuilder) Build(c *fi.CloudupModelBuilderContext) error {
+ if !b.UseServiceAccountExternalPermissions() {
+ return nil
+ }
+
+ issuerURL := fi.ValueOf(b.Cluster.Spec.KubeAPIServer.ServiceAccountIssuer)
+ if issuerURL == "" {
+ return fmt.Errorf("serviceAccountIssuer must be set for Azure Workload Identity")
+ }
+
+ rgName := b.Cluster.AzureResourceGroupName()
+ subscriptionID := b.Cluster.Spec.CloudProvider.Azure.SubscriptionID
+ identityName := b.Cluster.AzureWorkloadIdentityName()
+
+ // UAMI task โ idempotent, may already exist from pre-flight in apply_cluster.go.
+ uami := &azuretasks.ManagedIdentity{
+ Name: fi.PtrTo(identityName),
+ Lifecycle: b.Lifecycle,
+ ResourceGroup: b.LinkToResourceGroup(),
+ Tags: map[string]*string{},
+ }
+ c.AddTask(uami)
+
+ // Role assignment: Contributor on the resource group.
+ resourceGroupScope := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s", subscriptionID, rgName)
+ c.AddTask(&azuretasks.RoleAssignment{
+ Name: fi.PtrTo("wi-uami-contributor"),
+ Lifecycle: b.Lifecycle,
+ Scope: fi.PtrTo(resourceGroupScope),
+ ManagedIdentity: uami,
+ RoleDefID: fi.PtrTo(azureContributorRoleDefID),
+ })
+
+ // Federated Identity Credentials โ one per service account.
+ saBindings := []struct {
+ name string
+ namespace string
+ sa string
+ }{
+ {name: "fic-ccm", namespace: "kube-system", sa: "cloud-controller-manager"},
+ {name: "fic-csi-azuredisk", namespace: "kube-system", sa: "csi-azuredisk-controller-sa"},
+ }
+
+ for _, binding := range saBindings {
+ c.AddTask(&azuretasks.FederatedIdentityCredential{
+ Name: fi.PtrTo(binding.name),
+ Lifecycle: b.Lifecycle,
+ ManagedIdentity: uami,
+ ResourceGroup: b.LinkToResourceGroup(),
+ Issuer: fi.PtrTo(issuerURL),
+ Subject: fi.PtrTo(fmt.Sprintf("system:serviceaccount:%s:%s", binding.namespace, binding.sa)),
+ Audiences: []*string{fi.PtrTo("api://AzureADTokenExchange")},
+ })
+ }
+
+ return nil
+}
WorkloadIdentity Model Builder โ Issues
-
high
The role assignment name is hardcoded to 'wi-uami-contributor'. If two kops clusters share the same Azure resource group (a supported topology), their role assignment tasks will collide on this name, causing one cluster's apply to overwrite or conflict with the other's. The name should incorporate the cluster name or identity name to be unique per cluster.
-
medium
Granting the Contributor role on the entire resource group scope is very broad. Contributor allows creating/deleting any resource in the RG. A least-privilege approach would scope the role to only the specific resource types the CCM and CSI driver need (e.g., a custom role or Network Contributor + Disk contributor). This is a security concern for production clusters.
-
medium
Only two service accounts (CCM and CSI azuredisk) receive federated identity credentials. If customers use other Azure-integrated components (external-dns, cluster-autoscaler, CSI blob/file drivers), they will silently fail to authenticate via workload identity. This scope should be documented as a known limitation or the list should be configurable.
-
low
The copyright header says '2026' which is a future year and likely a typo โ it should be 2024 or 2025.
-
low
There are no unit tests for WorkloadIdentityModelBuilder.Build(). Given that this builder wires up security-sensitive resources, at minimum a table-driven test covering the guard clause (UseServiceAccountExternalPermissions=false), missing issuer, and the happy path should be added.
-
low
The Tags field on ManagedIdentity is set to an empty map literal (map[string]*string{}). Other builders typically propagate cluster tags for cost allocation and lifecycle management. This is probably an oversight rather than intentional.
Addon Manifest Templates (CCM & azuredisk-csi)
upup/models/cloudup/resources/addons. When Workload Identity is active the CCM secret gains useFederatedWorkloadIdentityExtension: true and an empty aadClientID, the CCM Deployment gets a projected serviceAccountToken volume for the Azure identity token, and the CSI controller Deployment similarly gets a projected token volume so both components can use OIDC federation instead of a client secret.Addon Manifest Templates (CCM & azuredisk-csi) โ Key Signatures
| Name | File | What it does |
|---|---|---|
UseServiceAccountExternalPermissions (template gate) โ CCM Secret block |
upup/models/cloudup/resources/addons/azure-cloud-controller.addons.k8s.io/k8s-1.31.yaml.template | Conditionally emits a Kubernetes Secret named azure-cloud-provider containing the CCM cloud-config JSON with useFederatedWorkloadIdentityExtension=true and aadFederatedTokenFile pointing to the projected token path, replacing the host-path-based MSI approach. |
UseServiceAccountExternalPermissions (template gate) โ CCM volumeMounts/volumes |
upup/models/cloudup/resources/addons/azure-cloud-controller.addons.k8s.io/k8s-1.31.yaml.template | Swaps the CCM Deployment's volume configuration between a Secret-backed azure-cloud-config mount (Workload Identity path) and a hostPath /etc/kubernetes + /var/lib/waagent/ManagedIdentity-Settings mount (legacy MSI path). |
UseServiceAccountExternalPermissions (template gate) โ azuredisk-csi projected token volume |
upup/models/cloudup/resources/addons/azuredisk-csi-driver.addons.k8s.io/k8s-1.31.yaml.template | Conditionally adds a projected serviceAccountToken volume (audience api://AzureADTokenExchange, 1-hour TTL) and corresponding volumeMount to the azuredisk-csi controller Deployment so it can exchange a Kubernetes service account token for an Azure AD token. |
Addon Manifest Templates (CCM & azuredisk-csi) โ Walkthrough
The change introduces a binary branching strategy inside both YAML templates: when UseServiceAccountExternalPermissions is true the components use Azure Workload Identity (OIDC federation), and when false they fall back to the existing Managed Identity / host-path approach, ensuring backwards compatibility for clusters that don't opt in.
For the CCM, the Workload Identity path emits a new Secret/azure-cloud-provider at the very top of the template (before the ServiceAccount) with the cloud-config JSON baked in; it sets useFederatedWorkloadIdentityExtension: true, aadFederatedTokenFile to the projected token path, and aadClientID to the workload identity client ID obtained via AzureWorkloadIdentityClientID.
The CCM Deployment's volumeMounts section then branches: under Workload Identity, /etc/kubernetes is served from the Secret (key cloud-config โ azure.json), and the MSI host-path mount is suppressed; under legacy, the original hostPath mounts for /etc/kubernetes and /var/lib/waagent/ManagedIdentity-Settings are retained unchanged.
The azure-identity-token projected volume (audience api://AzureADTokenExchange, 3600s expiry) already existed in the CCM template before this PR and is kept unconditionally โ only its corresponding volumeMount is now made conditional on Workload Identity, which raises a question about whether the volume definition should also be gated.
For the azuredisk-csi controller, the change is purely additive: a new projected serviceAccountToken volume and mount are injected only when UseServiceAccountExternalPermissions is true, using the same audience and expiration settings as the CCM, allowing the CSI driver to also exchange tokens with Azure AD via OIDC.
The CSI driver's existing azure-cred volume (a hostPath to /etc/kubernetes/) is left untouched in both paths, implying it still carries a cloud-config file on the node โ this may still reference MSI or client-secret settings depending on how the node is provisioned, so reviewers should verify that the CSI driver actually reads the token file when Workload Identity is active and ignores the legacy credentials in azure-cred.
Addon Manifest Templates (CCM & azuredisk-csi)
upup/models/cloudup/resources/addons/azure-cloud-controller.addons.k8s.io/k8s-1.31.yaml.template modified
@@ -1,3 +1,28 @@
+{{ if UseServiceAccountExternalPermissions }}
+apiVersion: v1
+kind: Secret
+metadata:
+ name: azure-cloud-provider
+ namespace: kube-system
+type: Opaque
+stringData:
+ cloud-config: |
+ {
+ "tenantId": "{{ AzureTenantID }}",
+ "subscriptionId": "{{ AzureSubscriptionID }}",
+ "resourceGroup": "{{ AzureResourceGroupName }}",
+ "location": "{{ AzureLocation }}",
+ "vnetName": "{{ AzureVnetName }}",
+ "subnetName": "{{ AzureSubnetName }}",
+ "securityGroupName": "{{ AzureSecurityGroupName }}",
+ "useFederatedWorkloadIdentityExtension": true,
+ "aadFederatedTokenFile": "/var/run/secrets/azure/tokens/azure-identity-token",
+ "aadClientID": "{{ AzureWorkloadIdentityClientID }}",
+ "useInstanceMetadata": true,
+ "disableAvailabilitySetNodes": true
+ }
+---
+{{ end }}
apiVersion: v1
kind: ServiceAccount
metadata:
@@ -244,14 +269,22 @@ spec:
cpu: '{{ or .ExternalCloudControllerManager.CPURequest "100m" }}'
memory: 128Mi
volumeMounts:
+{{ if UseServiceAccountExternalPermissions }}
+ - mountPath: /etc/kubernetes
+ name: azure-cloud-config
+ readOnly: true
+{{ else }}
- mountPath: /etc/kubernetes
name: etc-kubernetes
+{{ end }}
- mountPath: /etc/ssl
name: ssl-mount
readOnly: true
+{{ if not (UseServiceAccountExternalPermissions) }}
- mountPath: /var/lib/waagent/ManagedIdentity-Settings
name: msi
readOnly: true
+{{ end }}
- mountPath: /var/run/secrets/azure/tokens
name: azure-identity-token
readOnly: true
@@ -276,15 +309,26 @@ spec:
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: DoNotSchedule
volumes:
+{{ if UseServiceAccountExternalPermissions }}
+ - name: azure-cloud-config
+ secret:
+ secretName: azure-cloud-provider
+ items:
+ - key: cloud-config
+ path: azure.json
+{{ else }}
- hostPath:
path: /etc/kubernetes
name: etc-kubernetes
+{{ end }}
- hostPath:
path: /etc/ssl
name: ssl-mount
+{{ if not (UseServiceAccountExternalPermissions) }}
- hostPath:
path: /var/lib/waagent/ManagedIdentity-Settings
name: msi
+{{ end }}
- name: azure-identity-token
projected:
defaultMode: 420
Addon Manifest Templates (CCM & azuredisk-csi)
upup/models/cloudup/resources/addons/azuredisk-csi-driver.addons.k8s.io/k8s-1.31.yaml.template modified
@@ -746,6 +746,11 @@ spec:
name: socket-dir
- mountPath: /etc/kubernetes/
name: azure-cred
+{{ if UseServiceAccountExternalPermissions }}
+ - mountPath: /var/run/secrets/azure/tokens
+ name: azure-identity-token
+ readOnly: true
+{{ end }}
resources:
limits:
memory: 500Mi
@@ -763,6 +768,16 @@ spec:
hostPath:
path: /etc/kubernetes/
type: DirectoryOrCreate
+{{ if UseServiceAccountExternalPermissions }}
+ - name: azure-identity-token
+ projected:
+ defaultMode: 420
+ sources:
+ - serviceAccountToken:
+ audience: api://AzureADTokenExchange
+ expirationSeconds: 3600
+ path: azure-identity-token
+{{ end }}
---
# Source: azuredisk-csi-driver/templates/csi-azuredisk-driver.yaml
apiVersion: storage.k8s.io/v1
Addon Manifest Templates (CCM & azuredisk-csi) โ Issues
-
medium
In the CCM template, the
azure-identity-tokenprojected volume is defined unconditionally (outside anyif UseServiceAccountExternalPermissionsblock) but its corresponding volumeMount is only added under the Workload Identity branch. On non-Workload-Identity clusters this produces an orphaned volume definition with no mount, which is harmless but noisy. More importantly, if a future diff accidentally re-adds the mount unconditionally, the volume will be present either way. Consider wrapping the volume definition in the same guard as the mount. -
medium
The azuredisk-csi controller still mounts
azure-cred(a hostPath to/etc/kubernetes/) unconditionally in both Workload Identity and non-Workload-Identity paths. If the cloud-config file at that path on the node contains anaadClientSecretor MSI settings, the CSI driver may prefer those over the federated token. The template should either (a) gate the azure-cred mount similarly to how CCM gates its etc-kubernetes mount, or (b) document that the node-level azure-cred file must be updated to includeuseFederatedWorkloadIdentityExtension: truewhen this feature is enabled. -
low
The CCM Secret is rendered at the very top of the template before the
---separator, so the first document in the file is conditionally absent. Some YAML parsers or kops manifest loaders that assume a fixed document order may be confused when the first document is a ServiceAccount instead of a Secret. Verify that the template rendering pipeline handles variable document counts correctly. -
low
The cloud-config JSON in the Secret hardcodes
"disableAvailabilitySetNodes": truewithout any template variable or comment explaining the assumption. If a cluster uses availability sets rather than scale sets this will break node management. This field should either be templated or carry an explanatory comment. -
low
The projected serviceAccountToken TTL is 3600 seconds (1 hour) for both CCM and CSI. The Kubernetes kubelet will automatically rotate the token before expiry, but there is no documentation or comment in the templates explaining the chosen expiry or how to tune it. Adding a comment improves maintainability.
Integration Test: minimal_azure_wi
minimal_azure_wi that runs kops update cluster on a minimal Azure cluster with useServiceAccountExternalPermissions: true. The test registers TestMinimalAzureWi in cmd/kops/integration_test.go and provides the complete set of expected Terraform output blobs (VMSS user-data, cluster spec, OIDC discovery/keys JSON, CCM and CSI addon manifests) to lock in the rendered output and prevent silent regressions.- cmd/kops/integration_test.go
- tests/integration/update_cluster/minimal_azure_wi/data/azurerm_linux_virtual_machine_scale_set_control-plane-eastus-1.masters.minimal-azure.example.com_user_data
- tests/integration/update_cluster/minimal_azure_wi/data/azurerm_linux_virtual_machine_scale_set_nodes.minimal-azure.example.com_user_data
- tests/integration/update_cluster/minimal_azure_wi/data/azurerm_storage_blob_cluster-completed.spec_source
- tests/integration/update_cluster/minimal_azure_wi/data/azurerm_storage_blob_discovery.json_source
- tests/integration/update_cluster/minimal_azure_wi/data/azurerm_storage_blob_keys.json_source
- tests/integration/update_cluster/minimal_azure_wi/data/azurerm_storage_blob_minimal-azure.example.com-addons-azure-cloud-controller.addons.k8s.io-k8s-1.31_source
- tests/integration/update_cluster/minimal_azure_wi/data/azurerm_storage_blob_minimal-azure.example.com-addons-azuredisk-csi-driver.addons.k8s.io-k8s-1.31_source
- tests/integration/update_cluster/minimal_azure_wi/data/azurerm_storage_blob_etcd-cluster-spec-events_source
- tests/integration/update_cluster/minimal_azure_wi/data/azurerm_storage_blob_etcd-cluster-spec-main_source
- tests/integration/update_cluster/minimal_azure_wi/data/azurerm_storage_blob_kops-version.txt_source
- tests/integration/update_cluster/minimal_azure_wi/data/azurerm_storage_blob_manifests-etcdmanager-events-control-plane-eastus-1_source
- tests/integration/update_cluster/minimal_azure_wi/data/azurerm_storage_blob_manifests-etcdmanager-main-control-plane-eastus-1_source
- tests/integration/update_cluster/minimal_azure_wi/data/azurerm_storage_blob_manifests-static-kube-apiserver-healthcheck_source
Integration Test: minimal_azure_wi โ Key Signatures
| Name | File | What it does |
|---|---|---|
TestMinimalAzureWi |
cmd/kops/integration_test.go | Registers a new integration test that runs kops update cluster on a minimal Azure cluster with Workload Identity enabled and asserts the Terraform output matches the checked-in golden files. |
Integration Test: minimal_azure_wi โ Walkthrough
The test wires into the existing integration-test framework via newIntegrationTest("minimal-azure.example.com", "minimal_azure_wi").withVersion("v1alpha3").runTestTerraformAzure(t), which drives kops update cluster against an in-memory store, serialises all produced Terraform resources, and diffs them against the golden files under tests/integration/update_cluster/minimal_azure_wi/data/.
The core behavioural difference from TestMinimalAzure is the cluster spec field iam.useServiceAccountExternalPermissions: true, which activates the Workload Identity rendering path; the completed spec golden file confirms serviceAccountIssuer and serviceAccountJWKSURI are set, and apiAudiences includes kubernetes.svc.default.
The CCM addon golden file (azurerm_storage_blob_minimal-azure.example.com-addons-azure-cloud-controller.addons.k8s.io-k8s-1.31_source) is the most important WI-specific artifact: it shows useFederatedWorkloadIdentityExtension: true, aadFederatedTokenFile: /var/run/secrets/azure/tokens/azure-identity-token, and aadClientID: "" โ confirming no client-secret is injected โ plus a projected serviceAccountToken volume with audience api://AzureADTokenExchange on the CCM Deployment.
Similarly, the CSI controller Deployment golden file carries the same azure-identity-token projected volume mount on the azuredisk container, locking in that both the CCM and the azuredisk-csi controller use WI token projection rather than a client credential Secret.
The OIDC golden files (discovery.json and keys.json) capture the issuer URL https://discovery.example.com/minimal-azure.example.com and the two test RSA public keys, ensuring the OIDC discovery document and JWKS blob are rendered correctly when WI is active.
The user-data scripts for both the control-plane and node VMSS are essentially identical to the non-WI scenario (bootstrapping nodeup via kube_env.yaml), which is expected because WI only affects in-cluster addon configuration, not the node bootstrap process itself.
The etcd-manager pod manifests and healthcheck sidecar are also captured, so any accidental collateral changes to those resources will be caught even though they are unrelated to WI.
Integration Test: minimal_azure_wi
cmd/kops/integration_test.go modified
@@ -330,6 +330,17 @@ func TestMinimalAzure(t *testing.T) {
runTestTerraformAzure(t)
}
+// TestMinimalAzureWi runs the test on a minimum Azure configuration with
+// Workload Identity (useServiceAccountExternalPermissions) enabled. This pins
+// the rendered CCM and azuredisk-csi addon manifests on the workload-identity
+// branch so future refactors can't silently break the aadClientID secret or
+// the projected ServiceAccountToken volume.
+func TestMinimalAzureWi(t *testing.T) {
+ newIntegrationTest("minimal-azure.example.com", "minimal_azure_wi").
+ withVersion("v1alpha3").
+ runTestTerraformAzure(t)
+}
+
// TestMinimal_NoneDNS runs the test on a minimum configuration with --dns=none
func TestMinimal_NoneDNS(t *testing.T) {
newIntegrationTest("minimal.example.com", "minimal-dns-none").
Integration Test: minimal_azure_wi
@@ -0,0 +1,134 @@
+#!/bin/bash
+set -o errexit
+set -o nounset
+set -o pipefail
+
+NODEUP_URL_AMD64=https://artifacts.k8s.io/binaries/kops/1.34.0-beta.1/linux/amd64/nodeup,https://github.com/kubernetes/kops/releases/download/v1.34.0-beta.1/nodeup-linux-amd64
+NODEUP_HASH_AMD64=c86e072f622b91546b7b3f3cb1a0f8a131e48b966ad018a0ac1520ceedf37725
+NODEUP_URL_ARM64=https://artifacts.k8s.io/binaries/kops/1.34.0-beta.1/linux/arm64/nodeup,https://github.com/kubernetes/kops/releases/download/v1.34.0-beta.1/nodeup-linux-arm64
+NODEUP_HASH_ARM64=64a9a9510538a449e85d05e13e3cd98b80377d68a673447c26821d40f00f0075
+
+export AZURE_STORAGE_ACCOUNT=teststorage
+
+
+
+
+sysctl -w net.core.rmem_max=16777216 || true
+sysctl -w net.core.wmem_max=16777216 || true
+sysctl -w net.ipv4.tcp_rmem='4096 87380 16777216' || true
+sysctl -w net.ipv4.tcp_wmem='4096 87380 16777216' || true
+
+
+function ensure-install-dir() {
+ INSTALL_DIR="/opt/kops"
+ # On ContainerOS, we install under /var/lib/toolbox; /opt is ro and noexec
+ if [[ -d /var/lib/toolbox ]]; then
+ INSTALL_DIR="/var/lib/toolbox/kops"
+ fi
+ mkdir -p ${INSTALL_DIR}/bin
+ mkdir -p ${INSTALL_DIR}/conf
+ cd ${INSTALL_DIR}
+}
+
+# Retry a download until we get it. args: name, sha, urls
+download-or-bust() {
+ echo "== Downloading $1 with hash $2 from $3 =="
+ local -r file="$1"
+ local -r hash="$2"
+ local -a urls
+ IFS=, read -r -a urls <<< "$3"
+
+ if [[ -f "${file}" ]]; then
+ if ! validate-hash "${file}" "${hash}"; then
+ rm -f "${file}"
+ else
+ return 0
+ fi
+ fi
+
+ while true; do
+ for url in "${urls[@]}"; do
+ commands=(
+ "curl -f --compressed -Lo ${file} --connect-timeout 20 --retry 6 --retry-delay 10"
+ "wget --compression=auto -O ${file} --connect-timeout=20 --tries=6 --wait=10"
+ "curl -f -Lo ${file} --connect-timeout 20 --retry 6 --retry-delay 10"
+ "wget -O ${file} --connect-timeout=20 --tries=6 --wait=10"
+ )
+ for cmd in "${commands[@]}"; do
+ echo "== Downloading ${url} using ${cmd} =="
+ if ! (${cmd} "${url}"); then
+ echo "== Failed to download ${url} using ${cmd} =="
+ continue
+ fi
+ if ! validate-hash "${file}" "${hash}"; then
+ echo "== Failed to validate hash for ${url} =="
+ rm -f "${file}"
+ else
+ echo "== Downloaded ${url} with hash ${hash} =="
+ return 0
+ fi
+ done
+ done
+
+ echo "== All downloads failed; sleeping before retrying =="
+ sleep 60
+ done
+}
+
+validate-hash() {
+ local -r file="$1"
+ local -r expected="$2"
+ local actual
+
+ actual=$(sha256sum "${file}" | awk '{ print $1 }') || true
+ if [[ "${actual}" != "${expected}" ]]; then
+ echo "== File ${file} is corrupted; hash ${actual} doesn't match expected ${expected} =="
+ return 1
+ fi
+}
+
+function download-release() {
+ case "$(uname -m)" in
+ x86_64*|i?86_64*|amd64*)
+ NODEUP_URL="${NODEUP_URL_AMD64}"
+ NODEUP_HASH="${NODEUP_HASH_AMD64}"
+ ;;
+ aarch64*|arm64*)
+ NODEUP_URL="${NODEUP_URL_ARM64}"
+ NODEUP_HASH="${NODEUP_HASH_ARM64}"
+ ;;
+ *)
+ echo "Unsupported host arch: $(uname -m)" >&2
+ exit 1
+ ;;
+ esac
+
+ cd ${INSTALL_DIR}/bin
+ download-or-bust nodeup "${NODEUP_HASH}" "${NODEUP_URL}"
+
+ chmod +x nodeup
+
+ echo "== Running nodeup =="
+ # We can't run in the foreground because of https://github.com/docker/docker/issues/23793
+ ( cd ${INSTALL_DIR}/bin; ./nodeup --install-systemd-unit --conf=${INSTALL_DIR}/conf/kube_env.yaml --v=8 )
+}
+
+####################################################################################
+
+/bin/systemd-machine-id-setup || echo "== Failed to initialize the machine ID; ensure machine-id configured =="
+
+echo "== nodeup node config starting =="
+ensure-install-dir
+
+cat > conf/kube_env.yaml << '__EOF_KUBE_ENV'
+CloudProvider: azure
+ClusterName: minimal-azure.example.com
+ConfigBase: memfs://tests/minimal-azure.example.com
+InstanceGroupName: control-plane-eastus-1
+InstanceGroupRole: ControlPlane
+NodeupConfigHash: 4UV+G3bsNg4BjIo/PmocE7UsT0pVzwcZx5AROG8aKS8=
+
+__EOF_KUBE_ENV
+
+download-release
+echo "== nodeup node config done =="
Integration Test: minimal_azure_wi
@@ -0,0 +1,157 @@
+#!/bin/bash
+set -o errexit
+set -o nounset
+set -o pipefail
+
+NODEUP_URL_AMD64=https://artifacts.k8s.io/binaries/kops/1.34.0-beta.1/linux/amd64/nodeup,https://github.com/kubernetes/kops/releases/download/v1.34.0-beta.1/nodeup-linux-amd64
+NODEUP_HASH_AMD64=c86e072f622b91546b7b3f3cb1a0f8a131e48b966ad018a0ac1520ceedf37725
+NODEUP_URL_ARM64=https://artifacts.k8s.io/binaries/kops/1.34.0-beta.1/linux/arm64/nodeup,https://github.com/kubernetes/kops/releases/download/v1.34.0-beta.1/nodeup-linux-arm64
+NODEUP_HASH_ARM64=64a9a9510538a449e85d05e13e3cd98b80377d68a673447c26821d40f00f0075
+
+export AZURE_STORAGE_ACCOUNT=teststorage
+
+
+
+
+sysctl -w net.core.rmem_max=16777216 || true
+sysctl -w net.core.wmem_max=16777216 || true
+sysctl -w net.ipv4.tcp_rmem='4096 87380 16777216' || true
+sysctl -w net.ipv4.tcp_wmem='4096 87380 16777216' || true
+
+
+function ensure-install-dir() {
+ INSTALL_DIR="/opt/kops"
+ # On ContainerOS, we install under /var/lib/toolbox; /opt is ro and noexec
+ if [[ -d /var/lib/toolbox ]]; then
+ INSTALL_DIR="/var/lib/toolbox/kops"
+ fi
+ mkdir -p ${INSTALL_DIR}/bin
+ mkdir -p ${INSTALL_DIR}/conf
+ cd ${INSTALL_DIR}
+}
+
+# Retry a download until we get it. args: name, sha, urls
+download-or-bust() {
+ echo "== Downloading $1 with hash $2 from $3 =="
+ local -r file="$1"
+ local -r hash="$2"
+ local -a urls
+ IFS=, read -r -a urls <<< "$3"
+
+ if [[ -f "${file}" ]]; then
+ if ! validate-hash "${file}" "${hash}"; then
+ rm -f "${file}"
+ else
+ return 0
+ fi
+ fi
+
+ while true; do
+ for url in "${urls[@]}"; do
+ commands=(
+ "curl -f --compressed -Lo ${file} --connect-timeout 20 --retry 6 --retry-delay 10"
+ "wget --compression=auto -O ${file} --connect-timeout=20 --tries=6 --wait=10"
+ "curl -f -Lo ${file} --connect-timeout 20 --retry 6 --retry-delay 10"
+ "wget -O ${file} --connect-timeout=20 --tries=6 --wait=10"
+ )
+ for cmd in "${commands[@]}"; do
+ echo "== Downloading ${url} using ${cmd} =="
+ if ! (${cmd} "${url}"); then
+ echo "== Failed to download ${url} using ${cmd} =="
+ continue
+ fi
+ if ! validate-hash "${file}" "${hash}"; then
+ echo "== Failed to validate hash for ${url} =="
+ rm -f "${file}"
+ else
+ echo "== Downloaded ${url} with hash ${hash} =="
+ return 0
+ fi
+ done
+ done
+
+ echo "== All downloads failed; sleeping before retrying =="
+ sleep 60
+ done
+}
+
+validate-hash() {
+ local -r file="$1"
+ local -r expected="$2"
+ local actual
+
+ actual=$(sha256sum "${file}" | awk '{ print $1 }') || true
+ if [[ "${actual}" != "${expected}" ]]; then
+ echo "== File ${file} is corrupted; hash ${actual} doesn't match expected ${expected} =="
+ return 1
+ fi
+}
+
+function download-release() {
+ case "$(uname -m)" in
+ x86_64*|i?86_64*|amd64*)
+ NODEUP_URL="${NODEUP_URL_AMD64}"
+ NODEUP_HASH="${NODEUP_HASH_AMD64}"
+ ;;
+ aarch64*|arm64*)
+ NODEUP_URL="${NODEUP_URL_ARM64}"
+ NODEUP_HASH="${NODEUP_HASH_ARM64}"
+ ;;
+ *)
+ echo "Unsupported host arch: $(uname -m)" >&2
+ exit 1
+ ;;
+ esac
+
+ cd ${INSTALL_DIR}/bin
+ download-or-bust nodeup "${NODEUP_HASH}" "${NODEUP_URL}"
+
+ chmod +x nodeup
+
+ echo "== Running nodeup =="
+ # We can't run in the foreground because of https://github.com/docker/docker/issues/23793
+ ( cd ${INSTALL_DIR}/bin; ./nodeup --install-systemd-unit --conf=${INSTALL_DIR}/conf/kube_env.yaml --v=8 )
+}
+
+####################################################################################
+
+/bin/systemd-machine-id-setup || echo "== Failed to initialize the machine ID; ensure machine-id configured =="
+
+echo "== nodeup node config starting =="
+ensure-install-dir
+
+cat > conf/kube_env.yaml << '__EOF_KUBE_ENV'
+CloudProvider: azure
+ClusterName: minimal-azure.example.com
+ConfigServer:
+ CACertificates: |
+ -----BEGIN CERTIFICATE-----
+ MIIBbjCCARigAwIBAgIMFpANqBD8NSD82AUSMA0GCSqGSIb3DQEBCwUAMBgxFjAU
+ BgNVBAMTDWt1YmVybmV0ZXMtY2EwHhcNMjEwNzA3MDcwODAwWhcNMzEwNzA3MDcw
+ ODAwWjAYMRYwFAYDVQQDEw1rdWJlcm5ldGVzLWNhMFwwDQYJKoZIhvcNAQEBBQAD
+ SwAwSAJBANFI3zr0Tk8krsW8vwjfMpzJOlWQ8616vG3YPa2qAgI7V4oKwfV0yIg1
+ jt+H6f4P/wkPAPTPTfRp9Iy8oHEEFw0CAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgEG
+ MA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNG3zVjTcLlJwDsJ4/K9DV7KohUA
+ MA0GCSqGSIb3DQEBCwUAA0EAB8d03fY2w7WKpfO29qI295pu2C4ca9AiVGOpgSc8
+ tmQsq6rcxt3T+rb589PVtz0mw/cKTxOk6gH2CCC+yHfy2w==
+ -----END CERTIFICATE-----
+ -----BEGIN CERTIFICATE-----
+ MIIBbjCCARigAwIBAgIMFpANvmSa0OAlYmXKMA0GCSqGSIb3DQEBCwUAMBgxFjAU
+ BgNVBAMTDWt1YmVybmV0ZXMtY2EwHhcNMjEwNzA3MDcwOTM2WhcNMzEwNzA3MDcw
+ OTM2WjAYMRYwFAYDVQQDEw1rdWJlcm5ldGVzLWNhMFwwDQYJKoZIhvcNAQEBBQAD
+ SwAwSAJBAMF6F4aZdpe0RUpyykaBpWwZCnwbffhYGOw+fs6RdLuUq7QCNmJm/Eq7
+ WWOziMYDiI9SbclpD+6QiJ0N3EqppVUCAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgEG
+ MA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFLImp6ARjPDAH6nhI+scWVt3Q9bn
+ MA0GCSqGSIb3DQEBCwUAA0EAVQVx5MUtuAIeePuP9o51xtpT2S6Fvfi8J4ICxnlA
+ 9B7UD2ushcVFPtaeoL9Gfu8aY4KJBeqqg5ojl4qmRnThjw==
+ -----END CERTIFICATE-----
+ servers:
+ - https://kops-controller.internal.minimal-azure.example.com:3988/
+InstanceGroupName: nodes
+InstanceGroupRole: Node
+NodeupConfigHash: zEPsuZ/dS0S1DKOCx6k664pxqEVGq7XpVY1UJ0LRI4w=
+
+__EOF_KUBE_ENV
+
+download-release
+echo "== nodeup node config done =="
Integration Test: minimal_azure_wi
@@ -0,0 +1,200 @@
+apiVersion: kops.k8s.io/v1alpha2
+kind: Cluster
+metadata:
+ creationTimestamp: "2017-01-01T00:00:00Z"
+ name: minimal-azure.example.com
+spec:
+ api:
+ dns: {}
+ loadBalancer:
+ type: Public
+ authorization:
+ rbac: {}
+ channel: stable
+ cloudConfig:
+ azure:
+ adminUser: admin-user
+ storageAccountID: /subscriptions/sub-321/resourceGroups/resource-group-name/providers/Microsoft.Storage/storageAccounts/teststorage
+ subscriptionId: sub-123
+ tenantId: tenant-123
+ manageStorageClasses: true
+ cloudControllerManager:
+ azureNodeManagerImage: mcr.microsoft.com/oss/kubernetes/azure-cloud-node-manager:v1.34.3
+ image: mcr.microsoft.com/oss/kubernetes/azure-cloud-controller-manager:v1.34.3
+ leaderElection:
+ leaderElect: true
+ logLevel: 2
+ cloudProvider: azure
+ clusterDNSDomain: cluster.local
+ configBase: memfs://tests/minimal-azure.example.com
+ containerd:
+ logLevel: info
+ runc:
+ version: 1.3.4
+ sandboxImage: registry.k8s.io/pause:3.10.1
+ version: 2.1.6
+ etcdClusters:
+ - backups:
+ backupStore: memfs://tests/minimal-azure.example.com/backups/etcd/main
+ cpuRequest: 200m
+ etcdMembers:
+ - encryptedVolume: true
+ instanceGroup: control-plane-eastus-1
+ name: a
+ manager:
+ backupRetentionDays: 90
+ memoryRequest: 100Mi
+ name: main
+ version: 3.6.6
+ - backups:
+ backupStore: memfs://tests/minimal-azure.example.com/backups/etcd/events
+ cpuRequest: 100m
+ etcdMembers:
+ - encryptedVolume: true
+ instanceGroup: control-plane-eastus-1
+ name: a
+ manager:
+ backupRetentionDays: 90
+ memoryRequest: 100Mi
+ name: events
+ version: 3.6.6
+ iam:
+ legacy: false
+ useServiceAccountExternalPermissions: true
+ keyStore: memfs://tests/minimal-azure.example.com/pki
+ kubeAPIServer:
+ allowPrivileged: true
+ anonymousAuth: false
+ apiAudiences:
+ - kubernetes.svc.default
+ apiServerCount: 1
+ authorizationMode: Node,RBAC
+ bindAddress: 0.0.0.0
+ enableAdmissionPlugins:
+ - DefaultStorageClass
+ - DefaultTolerationSeconds
+ - LimitRanger
+ - MutatingAdmissionWebhook
+ - NamespaceLifecycle
+ - NodeRestriction
+ - ResourceQuota
+ - RuntimeClass
+ - ServiceAccount
+ - ValidatingAdmissionPolicy
+ - ValidatingAdmissionWebhook
+ etcdServers:
+ - https://127.0.0.1:4001
+ etcdServersOverrides:
+ - /events#https://127.0.0.1:4002
+ image: registry.k8s.io/kube-apiserver:v1.35.0
+ kubeletPreferredAddressTypes:
+ - InternalIP
+ - Hostname
+ - ExternalIP
+ logLevel: 2
+ requestheaderAllowedNames:
+ - aggregator
+ requestheaderExtraHeaderPrefixes:
+ - X-Remote-Extra-
+ requestheaderGroupHeaders:
+ - X-Remote-Group
+ requestheaderUsernameHeaders:
+ - X-Remote-User
+ securePort: 443
+ serviceAccountIssuer: https://discovery.example.com/minimal-azure.example.com
+ serviceAccountJWKSURI: https://discovery.example.com/minimal-azure.example.com/openid/v1/jwks
+ serviceClusterIPRange: 100.64.0.0/13
+ storageBackend: etcd3
+ kubeControllerManager:
+ allocateNodeCIDRs: true
+ attachDetachReconcileSyncPeriod: 1m0s
+ cloudProvider: external
+ clusterCIDR: 100.96.0.0/11
+ clusterName: minimal-azure.example.com
+ configureCloudRoutes: false
+ image: registry.k8s.io/kube-controller-manager:v1.35.0
+ leaderElection:
+ leaderElect: true
+ logLevel: 2
+ useServiceAccountCredentials: true
+ kubeDNS:
+ cacheMaxConcurrent: 150
+ cacheMaxSize: 1000
+ cpuRequest: 100m
+ domain: cluster.local
+ memoryLimit: 170Mi
+ memoryRequest: 70Mi
+ nodeLocalDNS:
+ cpuRequest: 25m
+ enabled: false
+ image: registry.k8s.io/dns/k8s-dns-node-cache:1.26.0
+ memoryRequest: 5Mi
+ provider: CoreDNS
+ serverIP: 100.64.0.10
+ kubeProxy:
+ clusterCIDR: 100.96.0.0/11
+ cpuRequest: 100m
+ image: registry.k8s.io/kube-proxy:v1.35.0
+ logLevel: 2
+ kubeScheduler:
+ image: registry.k8s.io/kube-scheduler:v1.35.0
+ leaderElection:
+ leaderElect: true
+ logLevel: 2
+ kubelet:
+ anonymousAuth: false
+ cgroupDriver: systemd
+ cgroupRoot: /
+ cloudProvider: external
+ clusterDNS: 100.64.0.10
+ clusterDomain: cluster.local
+ enableDebuggingHandlers: true
+ evictionHard: memory.available<100Mi,nodefs.available<10%,nodefs.inodesFree<5%,imagefs.available<10%,imagefs.inodesFree<5%
+ kubeconfigPath: /var/lib/kubelet/kubeconfig
+ logLevel: 2
+ podManifestPath: /etc/kubernetes/manifests
+ protectKernelDefaults: true
+ shutdownGracePeriod: 30s
+ shutdownGracePeriodCriticalPods: 10s
+ kubernetesApiAccess:
+ - 0.0.0.0/0
+ - ::/0
+ kubernetesVersion: 1.35.0
+ masterKubelet:
+ anonymousAuth: false
+ cgroupDriver: systemd
+ cgroupRoot: /
+ cloudProvider: external
+ clusterDNS: 100.64.0.10
+ clusterDomain: cluster.local
+ enableDebuggingHandlers: true
+ evictionHard: memory.available<100Mi,nodefs.available<10%,nodefs.inodesFree<5%,imagefs.available<10%,imagefs.inodesFree<5%
+ featureGates:
+ ImageVolume: "true"
+ kubeconfigPath: /var/lib/kubelet/kubeconfig
+ logLevel: 2
+ podManifestPath: /etc/kubernetes/manifests
+ protectKernelDefaults: true
+ shutdownGracePeriod: 30s
+ shutdownGracePeriodCriticalPods: 10s
+ masterPublicName: api.minimal-azure.example.com
+ networkCIDR: 10.0.0.0/16
+ networking:
+ cni: {}
+ nonMasqueradeCIDR: 100.64.0.0/10
+ podCIDR: 100.96.0.0/11
+ secretStore: memfs://tests/minimal-azure.example.com/secrets
+ serviceAccountIssuerDiscovery:
+ discoveryStore: memfs://discovery.example.com/minimal-azure.example.com
+ serviceClusterIPRange: 100.64.0.0/13
+ sshAccess:
+ - 0.0.0.0/0
+ - ::/0
+ subnets:
+ - cidr: 10.0.0.0/24
+ name: eastus
+ region: eastus
+ type: Public
+ topology:
+ dns:
+ type: None
Integration Test: minimal_azure_wi
tests/integration/update_cluster/minimal_azure_wi/data/azurerm_storage_blob_discovery.json_source added
@@ -0,0 +1,18 @@
+{
+"issuer": "https://discovery.example.com/minimal-azure.example.com",
+"jwks_uri": "https://discovery.example.com/minimal-azure.example.com/openid/v1/jwks",
+"authorization_endpoint": "urn:kubernetes:programmatic_authorization",
+"response_types_supported": [
+"id_token"
+],
+"subject_types_supported": [
+"public"
+],
+"id_token_signing_alg_values_supported": [
+"RS256"
+],
+"claims_supported": [
+"sub",
+"iss"
+]
+}
Integration Test: minimal_azure_wi
@@ -0,0 +1,4 @@
+{
+ "memberCount": 1,
+ "etcdVersion": "3.6.6"
+}
Integration Test: minimal_azure_wi
@@ -0,0 +1,4 @@
+{
+ "memberCount": 1,
+ "etcdVersion": "3.6.6"
+}
Integration Test: minimal_azure_wi
tests/integration/update_cluster/minimal_azure_wi/data/azurerm_storage_blob_keys.json_source added
@@ -0,0 +1,20 @@
+{
+"keys": [
+{
+"use": "sig",
+"kty": "RSA",
+"kid": "3mNcULfgtWECYyZWY5ow1rOHjiRwEZHx28HQcRec3Ew",
+"alg": "RS256",
+"n": "2JbeF8dNwqfEKKD65aGlVs58fWkA0qZdVLKw8qATzRBJTi1nqbj2kAR4gyy_C8Mxouxva_om9d7Sq8Ka55T7-w",
+"e": "AQAB"
+},
+{
+"use": "sig",
+"kty": "RSA",
+"kid": "G-cZ10iKJqrXhR15ivI7Lg2q_cuL0zN9ouL0vF67FLc",
+"alg": "RS256",
+"n": "o4Tridlsf4Yz3UAiup_scSTiG_OqxkUW3Fz7zGKvVcLeYj9GEIKuzoB1VFk1nboDq4cCuGLfdzaQdCQKPIsDuw",
+"e": "AQAB"
+}
+]
+}
Integration Test: minimal_azure_wi
tests/integration/update_cluster/minimal_azure_wi/data/azurerm_storage_blob_kops-version.txt_source added
@@ -0,0 +1 @@
+1.34.0-beta.1
Integration Test: minimal_azure_wi
@@ -0,0 +1,115 @@
+apiVersion: v1
+kind: Pod
+metadata:
+ labels:
+ k8s-app: etcd-manager-events
+ name: etcd-manager-events
+ namespace: kube-system
+spec:
+ containers:
+ - command:
+ - /bin/sh
+ - -c
+ - mkfifo /tmp/pipe; (tee -a /var/log/etcd.log < /tmp/pipe & ) ; exec /ko-app/etcd-manager
+ --backup-store=memfs://tests/minimal-azure.example.com/backups/etcd/events --client-urls=https://__name__:4002
+ --cluster-name=etcd-events --containerized=true --dns-suffix=.internal.minimal-azure.example.com
+ --grpc-port=3997 --peer-urls=https://__name__:2381 --quarantine-client-urls=https://__name__:3995
+ --v=6 --volume-name-tag=k8s.io_etcd_events --volume-provider=azure --volume-tag=k8s.io_etcd_events
+ --volume-tag=k8s.io_role_control_plane=1 --volume-tag=kubernetes.io_cluster_minimal-azure.example.com=owned
+ > /tmp/pipe 2>&1
+ env:
+ - name: AZURE_STORAGE_ACCOUNT
+ value: teststorage
+ - name: ETCD_MANAGER_DAILY_BACKUPS_RETENTION
+ value: 90d
+ image: registry.k8s.io/etcd-manager/etcd-manager-slim:v3.0.20260227
+ name: etcd-manager
+ resources:
+ requests:
+ cpu: 100m
+ memory: 100Mi
+ securityContext:
+ privileged: true
+ volumeMounts:
+ - mountPath: /rootfs
+ name: rootfs
+ - mountPath: /run
+ name: run
+ - mountPath: /etc/kubernetes/pki/etcd-manager
+ name: pki
+ - mountPath: /opt
+ name: opt
+ - mountPath: /opt/etcd-v3.4.13
+ name: etcd-v3-4-13
+ - mountPath: /opt/etcd-v3.4.3
+ name: etcd-v3-4-13
+ - mountPath: /opt/etcd-v3.5.0
+ name: etcd-v3-5-25
+ - mountPath: /opt/etcd-v3.5.1
+ name: etcd-v3-5-25
+ - mountPath: /opt/etcd-v3.5.13
+ name: etcd-v3-5-25
+ - mountPath: /opt/etcd-v3.5.17
+ name: etcd-v3-5-25
+ - mountPath: /opt/etcd-v3.5.21
+ name: etcd-v3-5-25
+ - mountPath: /opt/etcd-v3.5.23
+ name: etcd-v3-5-25
+ - mountPath: /opt/etcd-v3.5.24
+ name: etcd-v3-5-25
+ - mountPath: /opt/etcd-v3.5.25
+ name: etcd-v3-5-25
+ - mountPath: /opt/etcd-v3.5.3
+ name: etcd-v3-5-25
+ - mountPath: /opt/etcd-v3.5.4
+ name: etcd-v3-5-25
+ - mountPath: /opt/etcd-v3.5.6
+ name: etcd-v3-5-25
+ - mountPath: /opt/etcd-v3.5.7
+ name: etcd-v3-5-25
+ - mountPath: /opt/etcd-v3.5.9
+ name: etcd-v3-5-25
+ - mountPath: /opt/etcd-v3.6.5
+ name: etcd-v3-6-6
+ - mountPath: /opt/etcd-v3.6.6
+ name: etcd-v3-6-6
+ - mountPath: /var/log/etcd.log
+ name: varlogetcd
+ hostNetwork: true
+ hostPID: true
+ priorityClassName: system-cluster-critical
+ tolerations:
+ - key: CriticalAddonsOnly
+ operator: Exists
+ volumes:
+ - hostPath:
+ path: /
+ type: Directory
+ name: rootfs
+ - hostPath:
+ path: /run
+ type: DirectoryOrCreate
+ name: run
+ - hostPath:
+ path: /etc/kubernetes/pki/etcd-manager-events
+ type: DirectoryOrCreate
+ name: pki
+ - emptyDir: {}
+ name: opt
+ - image:
+ pullPolicy: IfNotPresent
+ reference: registry.k8s.io/etcd:v3.4.13
+ name: etcd-v3-4-13
+ - image:
+ pullPolicy: IfNotPresent
+ reference: registry.k8s.io/etcd:v3.5.25
+ name: etcd-v3-5-25
+ - image:
+ pullPolicy: IfNotPresent
+ reference: registry.k8s.io/etcd:v3.6.6
+ name: etcd-v3-6-6
+ - hostPath:
+ path: /var/log/etcd-events.log
+ type: FileOrCreate
+ name: varlogetcd
+status: {}
Integration Test: minimal_azure_wi
@@ -0,0 +1,115 @@
+apiVersion: v1
+kind: Pod
+metadata:
+ labels:
+ k8s-app: etcd-manager-main
+ name: etcd-manager-main
+ namespace: kube-system
+spec:
+ containers:
+ - command:
+ - /bin/sh
+ - -c
+ - mkfifo /tmp/pipe; (tee -a /var/log/etcd.log < /tmp/pipe & ) ; exec /ko-app/etcd-manager
+ --backup-store=memfs://tests/minimal-azure.example.com/backups/etcd/main --client-urls=https://__name__:4001
+ --cluster-name=etcd --containerized=true --dns-suffix=.internal.minimal-azure.example.com
+ --grpc-port=3996 --peer-urls=https://__name__:2380 --quarantine-client-urls=https://__name__:3994
+ --v=6 --volume-name-tag=k8s.io_etcd_main --volume-provider=azure --volume-tag=k8s.io_etcd_main
+ --volume-tag=k8s.io_role_control_plane=1 --volume-tag=kubernetes.io_cluster_minimal-azure.example.com=owned
+ > /tmp/pipe 2>&1
+ env:
+ - name: AZURE_STORAGE_ACCOUNT
+ value: teststorage
+ - name: ETCD_MANAGER_DAILY_BACKUPS_RETENTION
+ value: 90d
+ image: registry.k8s.io/etcd-manager/etcd-manager-slim:v3.0.20260227
+ name: etcd-manager
+ resources:
+ requests:
+ cpu: 200m
+ memory: 100Mi
+ securityContext:
+ privileged: true
+ volumeMounts:
+ - mountPath: /rootfs
+ name: rootfs
+ - mountPath: /run
+ name: run
+ - mountPath: /etc/kubernetes/pki/etcd-manager
+ name: pki
+ - mountPath: /opt
+ name: opt
+ - mountPath: /opt/etcd-v3.4.13
+ name: etcd-v3-4-13
+ - mountPath: /opt/etcd-v3.4.3
+ name: etcd-v3-4-13
+ - mountPath: /opt/etcd-v3.5.0
+ name: etcd-v3-5-25
+ - mountPath: /opt/etcd-v3.5.1
+ name: etcd-v3-5-25
+ - mountPath: /opt/etcd-v3.5.13
+ name: etcd-v3-5-25
+ - mountPath: /opt/etcd-v3.5.17
+ name: etcd-v3-5-25
+ - mountPath: /opt/etcd-v3.5.21
+ name: etcd-v3-5-25
+ - mountPath: /opt/etcd-v3.5.23
+ name: etcd-v3-5-25
+ - mountPath: /opt/etcd-v3.5.24
+ name: etcd-v3-5-25
+ - mountPath: /opt/etcd-v3.5.25
+ name: etcd-v3-5-25
+ - mountPath: /opt/etcd-v3.5.3
+ name: etcd-v3-5-25
+ - mountPath: /opt/etcd-v3.5.4
+ name: etcd-v3-5-25
+ - mountPath: /opt/etcd-v3.5.6
+ name: etcd-v3-5-25
+ - mountPath: /opt/etcd-v3.5.7
+ name: etcd-v3-5-25
+ - mountPath: /opt/etcd-v3.5.9
+ name: etcd-v3-5-25
+ - mountPath: /opt/etcd-v3.6.5
+ name: etcd-v3-6-6
+ - mountPath: /opt/etcd-v3.6.6
+ name: etcd-v3-6-6
+ - mountPath: /var/log/etcd.log
+ name: varlogetcd
+ hostNetwork: true
+ hostPID: true
+ priorityClassName: system-cluster-critical
+ tolerations:
+ - key: CriticalAddonsOnly
+ operator: Exists
+ volumes:
+ - hostPath:
+ path: /
+ type: Directory
+ name: rootfs
+ - hostPath:
+ path: /run
+ type: DirectoryOrCreate
+ name: run
+ - hostPath:
+ path: /etc/kubernetes/pki/etcd-manager-main
+ type: DirectoryOrCreate
+ name: pki
+ - emptyDir: {}
+ name: opt
+ - image:
+ pullPolicy: IfNotPresent
+ reference: registry.k8s.io/etcd:v3.4.13
+ name: etcd-v3-4-13
+ - image:
+ pullPolicy: IfNotPresent
+ reference: registry.k8s.io/etcd:v3.5.25
+ name: etcd-v3-5-25
+ - image:
+ pullPolicy: IfNotPresent
+ reference: registry.k8s.io/etcd:v3.6.6
+ name: etcd-v3-6-6
+ - hostPath:
+ path: /var/log/etcd.log
+ type: FileOrCreate
+ name: varlogetcd
+status: {}
Integration Test: minimal_azure_wi
@@ -0,0 +1,32 @@
+apiVersion: v1
+kind: Pod
+metadata: {}
+spec:
+ containers:
+ - args:
+ - --ca-cert=/secrets/ca.crt
+ - --client-cert=/secrets/client.crt
+ - --client-key=/secrets/client.key
+ image: registry.k8s.io/kops/kube-apiserver-healthcheck:1.34.0-beta.1
+ livenessProbe:
+ httpGet:
+ host: 127.0.0.1
+ path: /.kube-apiserver-healthcheck/healthz
+ port: 3990
+ initialDelaySeconds: 5
+ timeoutSeconds: 5
+ name: healthcheck
+ resources: {}
+ securityContext:
+ runAsNonRoot: true
+ runAsUser: 10012
+ volumeMounts:
+ - mountPath: /secrets
+ name: healthcheck-secrets
+ readOnly: true
+ volumes:
+ - hostPath:
+ path: /etc/kubernetes/kube-apiserver-healthcheck/secrets
+ type: Directory
+ name: healthcheck-secrets
+status: {}
Integration Test: minimal_azure_wi
@@ -0,0 +1,426 @@
+apiVersion: v1
+kind: Secret
+metadata:
+ labels:
+ addon.kops.k8s.io/name: azure-cloud-controller.addons.k8s.io
+ app.kubernetes.io/managed-by: kops
+ k8s-addon: azure-cloud-controller.addons.k8s.io
+ name: azure-cloud-provider
+ namespace: kube-system
+stringData:
+ cloud-config: |-
+ {
+ "tenantId": "tenant-123",
+ "subscriptionId": "sub-123",
+ "resourceGroup": "minimal-azure.example.com",
+ "location": "eastus",
+ "vnetName": "minimal-azure.example.com",
+ "subnetName": "eastus",
+ "securityGroupName": "minimal-azure.example.com",
+ "useFederatedWorkloadIdentityExtension": true,
+ "aadFederatedTokenFile": "/var/run/secrets/azure/tokens/azure-identity-token",
+ "aadClientID": "",
+ "useInstanceMetadata": true,
+ "disableAvailabilitySetNodes": true
+ }
+type: Opaque
+
+---
+
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ labels:
+ addon.kops.k8s.io/name: azure-cloud-controller.addons.k8s.io
+ app.kubernetes.io/managed-by: kops
+ k8s-addon: azure-cloud-controller.addons.k8s.io
+ name: cloud-controller-manager
+ namespace: kube-system
+
+---
+
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ labels:
+ addon.kops.k8s.io/name: azure-cloud-controller.addons.k8s.io
+ app.kubernetes.io/managed-by: kops
+ k8s-addon: azure-cloud-controller.addons.k8s.io
+ k8s-app: cloud-node-manager
+ kubernetes.io/cluster-service: "true"
+ name: cloud-node-manager
+ namespace: kube-system
+
+---
+
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ labels:
+ addon.kops.k8s.io/name: azure-cloud-controller.addons.k8s.io
+ app.kubernetes.io/managed-by: kops
+ k8s-addon: azure-cloud-controller.addons.k8s.io
+ k8s-app: cloud-node-manager
+ kubernetes.io/cluster-service: "true"
+ name: cloud-node-manager
+rules:
+- apiGroups:
+ - ""
+ resources:
+ - nodes
+ verbs:
+ - watch
+ - list
+ - get
+ - update
+ - patch
+- apiGroups:
+ - ""
+ resources:
+ - nodes/status
+ verbs:
+ - patch
+
+---
+
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ annotations:
+ rbac.authorization.kubernetes.io/autoupdate: "true"
+ labels:
+ addon.kops.k8s.io/name: azure-cloud-controller.addons.k8s.io
+ app.kubernetes.io/managed-by: kops
+ k8s-addon: azure-cloud-controller.addons.k8s.io
+ k8s-app: cloud-controller-manager
+ name: system:cloud-controller-manager
+rules:
+- apiGroups:
+ - ""
+ resources:
+ - events
+ verbs:
+ - create
+ - patch
+ - update
+- apiGroups:
+ - ""
+ resources:
+ - nodes
+ verbs:
+ - '*'
+- apiGroups:
+ - ""
+ resources:
+ - nodes/status
+ verbs:
+ - patch
+- apiGroups:
+ - ""
+ resources:
+ - services
+ verbs:
+ - list
+ - patch
+ - update
+ - watch
+- apiGroups:
+ - ""
+ resources:
+ - services/status
+ verbs:
+ - list
+ - patch
+ - update
+ - watch
+- apiGroups:
+ - ""
+ resources:
+ - serviceaccounts
+ verbs:
+ - create
+ - get
+ - list
+ - watch
+ - update
+- apiGroups:
+ - ""
+ resources:
+ - persistentvolumes
+ verbs:
+ - get
+ - list
+ - update
+ - watch
+- apiGroups:
+ - ""
+ resources:
+ - endpoints
+ verbs:
+ - create
+ - get
+ - list
+ - watch
+ - update
+- apiGroups:
+ - ""
+ resources:
+ - secrets
+ verbs:
+ - get
+ - list
+ - watch
+- apiGroups:
+ - coordination.k8s.io
+ resources:
+ - leases
+ verbs:
+ - get
+ - create
+ - update
+- apiGroups:
+ - discovery.k8s.io
+ resources:
+ - endpointslices
+ verbs:
+ - get
+ - list
+ - watch
+
+---
+
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+ labels:
+ addon.kops.k8s.io/name: azure-cloud-controller.addons.k8s.io
+ app.kubernetes.io/managed-by: kops
+ k8s-addon: azure-cloud-controller.addons.k8s.io
+ name: system:cloud-controller-manager:extension-apiserver-authentication-reader
+ namespace: kube-system
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: Role
+ name: extension-apiserver-authentication-reader
+subjects:
+- kind: ServiceAccount
+ name: cloud-controller-manager
+ namespace: kube-system
+- apiGroup: ""
+ kind: User
+ name: cloud-controller-manager
+
+---
+
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+ labels:
+ addon.kops.k8s.io/name: azure-cloud-controller.addons.k8s.io
+ app.kubernetes.io/managed-by: kops
+ k8s-addon: azure-cloud-controller.addons.k8s.io
+ k8s-app: cloud-node-manager
+ kubernetes.io/cluster-service: "true"
+ name: cloud-node-manager
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: ClusterRole
+ name: cloud-node-manager
+subjects:
+- kind: ServiceAccount
+ name: cloud-node-manager
+ namespace: kube-system
+
+---
+
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+ labels:
+ addon.kops.k8s.io/name: azure-cloud-controller.addons.k8s.io
+ app.kubernetes.io/managed-by: kops
+ k8s-addon: azure-cloud-controller.addons.k8s.io
+ name: system:cloud-controller-manager
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: ClusterRole
+ name: system:cloud-controller-manager
+subjects:
+- kind: ServiceAccount
+ name: cloud-controller-manager
+ namespace: kube-system
+- kind: User
+ name: cloud-controller-manager
+
+---
+
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ labels:
+ addon.kops.k8s.io/name: azure-cloud-controller.addons.k8s.io
+ app.kubernetes.io/managed-by: kops
+ component: cloud-controller-manager
+ k8s-addon: azure-cloud-controller.addons.k8s.io
+ name: cloud-controller-manager
+ namespace: kube-system
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ component: cloud-controller-manager
+ tier: control-plane
+ template:
+ metadata:
+ labels:
+ component: cloud-controller-manager
+ kops.k8s.io/managed-by: kops
+ tier: control-plane
+ spec:
+ containers:
+ - args:
+ - --allocate-node-cidrs=false
+ - --cloud-config=/etc/kubernetes/azure.json
+ - --cloud-provider=azure
+ - --cluster-cidr=100.96.0.0/11
+ - --cluster-name=minimal-azure.example.com
+ - --configure-cloud-routes=false
+ - --controllers=*,-cloud-node
+ - --leader-elect=true
+ - --route-reconciliation-period=10s
+ - --secure-port=10268
+ - --v=2
+ command:
+ - cloud-controller-manager
+ env:
+ - name: KUBERNETES_SERVICE_HOST
+ value: 127.0.0.1
+ image: mcr.microsoft.com/oss/kubernetes/azure-cloud-controller-manager:v1.34.3
+ imagePullPolicy: IfNotPresent
+ livenessProbe:
+ httpGet:
+ path: /healthz
+ port: 10268
+ scheme: HTTPS
+ initialDelaySeconds: 20
+ periodSeconds: 10
+ timeoutSeconds: 5
+ name: cloud-controller-manager
+ resources:
+ limits:
+ cpu: 4
+ memory: 2Gi
+ requests:
+ cpu: 100m
+ memory: 128Mi
+ volumeMounts:
+ - mountPath: /etc/kubernetes
+ name: azure-cloud-config
+ readOnly: true
+ - mountPath: /etc/ssl
+ name: ssl-mount
+ readOnly: true
+ - mountPath: /var/run/secrets/azure/tokens
+ name: azure-identity-token
+ readOnly: true
+ hostNetwork: true
+ nodeSelector:
+ node-role.kubernetes.io/control-plane: ""
+ priorityClassName: system-node-critical
+ serviceAccountName: cloud-controller-manager
+ tolerations:
+ - effect: NoSchedule
+ key: node-role.kubernetes.io/master
+ - effect: NoSchedule
+ key: node-role.kubernetes.io/control-plane
+ - effect: NoExecute
+ key: node-role.kubernetes.io/etcd
+ topologySpreadConstraints:
+ - labelSelector:
+ matchLabels:
+ component: cloud-controller-manager
+ tier: control-plane
+ maxSkew: 1
+ topologyKey: kubernetes.io/hostname
+ whenUnsatisfiable: DoNotSchedule
+ volumes:
+ - name: azure-cloud-config
+ secret:
+ items:
+ - key: cloud-config
+ path: azure.json
+ secretName: azure-cloud-provider
+ - hostPath:
+ path: /etc/ssl
+ name: ssl-mount
+ - name: azure-identity-token
+ projected:
+ defaultMode: 420
+ sources:
+ - serviceAccountToken:
+ audience: api://AzureADTokenExchange
+ expirationSeconds: 3600
+ path: azure-identity-token
+
+---
+
+apiVersion: apps/v1
+kind: DaemonSet
+metadata:
+ labels:
+ addon.kops.k8s.io/name: azure-cloud-controller.addons.k8s.io
+ app.kubernetes.io/managed-by: kops
+ component: cloud-node-manager
+ k8s-addon: azure-cloud-controller.addons.k8s.io
+ kubernetes.io/cluster-service: "true"
+ name: cloud-node-manager
+ namespace: kube-system
+spec:
+ selector:
+ matchLabels:
+ k8s-app: cloud-node-manager
+ template:
+ metadata:
+ annotations:
+ cluster-autoscaler.kubernetes.io/daemonset-pod: "true"
+ labels:
+ k8s-app: cloud-node-manager
+ kops.k8s.io/managed-by: kops
+ spec:
+ containers:
+ - args:
+ - --node-name=$(NODE_NAME)
+ - --v=2
+ command:
+ - cloud-node-manager
+ env:
+ - name: NODE_NAME
+ valueFrom:
+ fieldRef:
+ fieldPath: spec.nodeName
+ image: mcr.microsoft.com/oss/kubernetes/azure-cloud-node-manager:v1.34.3
+ imagePullPolicy: IfNotPresent
+ name: cloud-node-manager
+ resources:
+ limits:
+ cpu: 2
+ memory: 512Mi
+ requests:
+ cpu: 50m
+ memory: 50Mi
+ hostNetwork: true
+ nodeSelector:
+ kubernetes.io/os: linux
+ priorityClassName: system-node-critical
+ serviceAccountName: cloud-node-manager
+ tolerations:
+ - key: CriticalAddonsOnly
+ operator: Exists
+ - effect: NoSchedule
+ key: node-role.kubernetes.io/master
+ - effect: NoSchedule
+ key: node-role.kubernetes.io/control-plane
+ - effect: NoExecute
+ operator: Exists
+ - effect: NoSchedule
+ operator: Exists
Integration Test: minimal_azure_wi
@@ -0,0 +1,1044 @@
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ labels:
+ addon.kops.k8s.io/name: azuredisk-csi-driver.addons.k8s.io
+ app.kubernetes.io/instance: azuredisk-csi-driver
+ app.kubernetes.io/managed-by: kops
+ app.kubernetes.io/name: azuredisk-csi-driver
+ app.kubernetes.io/version: 1.34.0
+ helm.sh/chart: azuredisk-csi-driver-1.34.0
+ k8s-addon: azuredisk-csi-driver.addons.k8s.io
+ name: csi-azuredisk-controller-sa
+ namespace: kube-system
+
+---
+
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ labels:
+ addon.kops.k8s.io/name: azuredisk-csi-driver.addons.k8s.io
+ app.kubernetes.io/instance: azuredisk-csi-driver
+ app.kubernetes.io/managed-by: kops
+ app.kubernetes.io/name: azuredisk-csi-driver
+ app.kubernetes.io/version: 1.34.0
+ helm.sh/chart: azuredisk-csi-driver-1.34.0
+ k8s-addon: azuredisk-csi-driver.addons.k8s.io
+ name: csi-azuredisk-node-sa
+ namespace: kube-system
+
+---
+
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ labels:
+ addon.kops.k8s.io/name: azuredisk-csi-driver.addons.k8s.io
+ app.kubernetes.io/instance: azuredisk-csi-driver
+ app.kubernetes.io/managed-by: kops
+ app.kubernetes.io/name: azuredisk-csi-driver
+ app.kubernetes.io/version: 1.34.0
+ helm.sh/chart: azuredisk-csi-driver-1.34.0
+ k8s-addon: azuredisk-csi-driver.addons.k8s.io
+ name: azuredisk-external-provisioner-role
+rules:
+- apiGroups:
+ - ""
+ resources:
+ - persistentvolumes
+ verbs:
+ - get
+ - list
+ - watch
+ - create
+ - patch
+ - delete
+- apiGroups:
+ - ""
+ resources:
+ - persistentvolumeclaims
+ verbs:
+ - get
+ - list
+ - watch
+ - update
+- apiGroups:
+ - storage.k8s.io
+ resources:
+ - storageclasses
+ verbs:
+ - get
+ - list
+ - watch
+- apiGroups:
+ - ""
+ resources:
+ - events
+ verbs:
+ - get
+ - list
+ - watch
+ - create
+ - update
+ - patch
+- apiGroups:
+ - storage.k8s.io
+ resources:
+ - csinodes
+ verbs:
+ - get
+ - list
+ - watch
+- apiGroups:
+ - ""
+ resources:
+ - nodes
+ verbs:
+ - get
+ - list
+ - watch
+- apiGroups:
+ - snapshot.storage.k8s.io
+ resources:
+ - volumesnapshots
+ verbs:
+ - get
+ - list
+- apiGroups:
+ - snapshot.storage.k8s.io
+ resources:
+ - volumesnapshotcontents
+ verbs:
+ - get
+ - list
+- apiGroups:
+ - coordination.k8s.io
+ resources:
+ - leases
+ verbs:
+ - get
+ - watch
+ - list
+ - delete
+ - update
+ - create
+ - patch
+
+---
+
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ labels:
+ addon.kops.k8s.io/name: azuredisk-csi-driver.addons.k8s.io
+ app.kubernetes.io/instance: azuredisk-csi-driver
+ app.kubernetes.io/managed-by: kops
+ app.kubernetes.io/name: azuredisk-csi-driver
+ app.kubernetes.io/version: 1.34.0
+ helm.sh/chart: azuredisk-csi-driver-1.34.0
+ k8s-addon: azuredisk-csi-driver.addons.k8s.io
+ name: azuredisk-external-attacher-role
+rules:
+- apiGroups:
+ - ""
+ resources:
+ - persistentvolumes
+ verbs:
+ - get
+ - list
+ - watch
+ - update
+- apiGroups:
+ - ""
+ resources:
+ - nodes
+ verbs:
+ - get
+ - list
+ - watch
+- apiGroups:
+ - csi.storage.k8s.io
+ resources:
+ - csinodeinfos
+ verbs:
+ - get
+ - list
+ - watch
+- apiGroups:
+ - storage.k8s.io
+ resources:
+ - volumeattachments
+ verbs:
+ - get
+ - list
+ - watch
+ - update
+ - patch
+- apiGroups:
+ - storage.k8s.io
+ resources:
+ - volumeattachments/status
+ verbs:
+ - get
+ - list
+ - watch
+ - update
+ - patch
+- apiGroups:
+ - storage.k8s.io
+ resources:
+ - volumeattributesclasses
+ verbs:
+ - get
+- apiGroups:
+ - coordination.k8s.io
+ resources:
+ - leases
+ verbs:
+ - get
+ - watch
+ - list
+ - delete
+ - update
+ - create
+ - patch
+
+---
+
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ labels:
+ addon.kops.k8s.io/name: azuredisk-csi-driver.addons.k8s.io
+ app.kubernetes.io/instance: azuredisk-csi-driver
+ app.kubernetes.io/managed-by: kops
+ app.kubernetes.io/name: azuredisk-csi-driver
+ app.kubernetes.io/version: 1.34.0
+ helm.sh/chart: azuredisk-csi-driver-1.34.0
+ k8s-addon: azuredisk-csi-driver.addons.k8s.io
+ name: azuredisk-external-snapshotter-role
+rules:
+- apiGroups:
+ - ""
+ resources:
+ - events
+ verbs:
+ - list
+ - watch
+ - create
+ - update
+ - patch
+- apiGroups:
+ - ""
+ resources:
+ - secrets
+ verbs:
+ - get
+ - list
+- apiGroups:
+ - snapshot.storage.k8s.io
+ resources:
+ - volumesnapshotclasses
+ verbs:
+ - get
+ - list
+ - watch
+- apiGroups:
+ - snapshot.storage.k8s.io
+ resources:
+ - volumesnapshotcontents
+ verbs:
+ - create
+ - get
+ - list
+ - watch
+ - update
+ - delete
+ - patch
+- apiGroups:
+ - snapshot.storage.k8s.io
+ resources:
+ - volumesnapshotcontents/status
+ verbs:
+ - update
+ - patch
+- apiGroups:
+ - coordination.k8s.io
+ resources:
+ - leases
+ verbs:
+ - get
+ - watch
+ - list
+ - delete
+ - update
+ - create
+ - patch
+
+---
+
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ labels:
+ addon.kops.k8s.io/name: azuredisk-csi-driver.addons.k8s.io
+ app.kubernetes.io/instance: azuredisk-csi-driver
+ app.kubernetes.io/managed-by: kops
+ app.kubernetes.io/name: azuredisk-csi-driver
+ app.kubernetes.io/version: 1.34.0
+ helm.sh/chart: azuredisk-csi-driver-1.34.0
+ k8s-addon: azuredisk-csi-driver.addons.k8s.io
+ name: azuredisk-external-resizer-role
+rules:
+- apiGroups:
+ - ""
+ resources:
+ - persistentvolumes
+ verbs:
+ - get
+ - list
+ - watch
+ - update
+ - patch
+- apiGroups:
+ - ""
+ resources:
+ - persistentvolumeclaims
+ verbs:
+ - get
+ - list
+ - watch
+- apiGroups:
+ - ""
+ resources:
+ - persistentvolumeclaims/status
+ verbs:
+ - update
+ - patch
+- apiGroups:
+ - ""
+ resources:
+ - events
+ verbs:
+ - list
+ - watch
+ - create
+ - update
+ - patch
+- apiGroups:
+ - coordination.k8s.io
+ resources:
+ - leases
+ verbs:
+ - get
+ - watch
+ - list
+ - delete
+ - update
+ - create
+ - patch
+- apiGroups:
+ - ""
+ resources:
+ - pods
+ verbs:
+ - get
+ - list
+ - watch
+- apiGroups:
+ - storage.k8s.io
+ resources:
+ - volumeattributesclasses
+ verbs:
+ - get
+ - list
+ - watch
+
+---
+
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ labels:
+ addon.kops.k8s.io/name: azuredisk-csi-driver.addons.k8s.io
+ app.kubernetes.io/managed-by: kops
+ k8s-addon: azuredisk-csi-driver.addons.k8s.io
+ name: csi-azuredisk-controller-secret-role
+rules:
+- apiGroups:
+ - ""
+ resources:
+ - secrets
+ verbs:
+ - get
+
+---
+
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ labels:
+ addon.kops.k8s.io/name: azuredisk-csi-driver.addons.k8s.io
+ app.kubernetes.io/managed-by: kops
+ k8s-addon: azuredisk-csi-driver.addons.k8s.io
+ name: csi-azuredisk-node-role
+rules:
+- apiGroups:
+ - ""
+ resources:
+ - secrets
+ verbs:
+ - get
+- apiGroups:
+ - ""
+ resources:
+ - nodes
+ verbs:
+ - get
+ - patch
+- apiGroups:
+ - storage.k8s.io
+ resources:
+ - csinodes
+ verbs:
+ - get
+- apiGroups:
+ - storage.k8s.io
+ resources:
+ - volumeattachments
+ verbs:
+ - get
+ - list
+ - watch
+
+---
+
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+ labels:
+ addon.kops.k8s.io/name: azuredisk-csi-driver.addons.k8s.io
+ app.kubernetes.io/instance: azuredisk-csi-driver
+ app.kubernetes.io/managed-by: kops
+ app.kubernetes.io/name: azuredisk-csi-driver
+ app.kubernetes.io/version: 1.34.0
+ helm.sh/chart: azuredisk-csi-driver-1.34.0
+ k8s-addon: azuredisk-csi-driver.addons.k8s.io
+ name: azuredisk-csi-provisioner-binding
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: ClusterRole
+ name: azuredisk-external-provisioner-role
+subjects:
+- kind: ServiceAccount
+ name: csi-azuredisk-controller-sa
+ namespace: kube-system
+
+---
+
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+ labels:
+ addon.kops.k8s.io/name: azuredisk-csi-driver.addons.k8s.io
+ app.kubernetes.io/instance: azuredisk-csi-driver
+ app.kubernetes.io/managed-by: kops
+ app.kubernetes.io/name: azuredisk-csi-driver
+ app.kubernetes.io/version: 1.34.0
+ helm.sh/chart: azuredisk-csi-driver-1.34.0
+ k8s-addon: azuredisk-csi-driver.addons.k8s.io
+ name: azuredisk-csi-attacher-binding
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: ClusterRole
+ name: azuredisk-external-attacher-role
+subjects:
+- kind: ServiceAccount
+ name: csi-azuredisk-controller-sa
+ namespace: kube-system
+
+---
+
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+ labels:
+ addon.kops.k8s.io/name: azuredisk-csi-driver.addons.k8s.io
+ app.kubernetes.io/instance: azuredisk-csi-driver
+ app.kubernetes.io/managed-by: kops
+ app.kubernetes.io/name: azuredisk-csi-driver
+ app.kubernetes.io/version: 1.34.0
+ helm.sh/chart: azuredisk-csi-driver-1.34.0
+ k8s-addon: azuredisk-csi-driver.addons.k8s.io
+ name: azuredisk-csi-snapshotter-binding
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: ClusterRole
+ name: azuredisk-external-snapshotter-role
+subjects:
+- kind: ServiceAccount
+ name: csi-azuredisk-controller-sa
+ namespace: kube-system
+
+---
+
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+ labels:
+ addon.kops.k8s.io/name: azuredisk-csi-driver.addons.k8s.io
+ app.kubernetes.io/instance: azuredisk-csi-driver
+ app.kubernetes.io/managed-by: kops
+ app.kubernetes.io/name: azuredisk-csi-driver
+ app.kubernetes.io/version: 1.34.0
+ helm.sh/chart: azuredisk-csi-driver-1.34.0
+ k8s-addon: azuredisk-csi-driver.addons.k8s.io
+ name: azuredisk-csi-resizer-role
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: ClusterRole
+ name: azuredisk-external-resizer-role
+subjects:
+- kind: ServiceAccount
+ name: csi-azuredisk-controller-sa
+ namespace: kube-system
+
+---
+
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+ labels:
+ addon.kops.k8s.io/name: azuredisk-csi-driver.addons.k8s.io
+ app.kubernetes.io/managed-by: kops
+ k8s-addon: azuredisk-csi-driver.addons.k8s.io
+ name: csi-azuredisk-controller-secret-binding
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: ClusterRole
+ name: csi-azuredisk-controller-secret-role
+subjects:
+- kind: ServiceAccount
+ name: csi-azuredisk-controller-sa
+ namespace: kube-system
+
+---
+
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+ labels:
+ addon.kops.k8s.io/name: azuredisk-csi-driver.addons.k8s.io
+ app.kubernetes.io/managed-by: kops
+ k8s-addon: azuredisk-csi-driver.addons.k8s.io
+ name: csi-azuredisk-node-secret-binding
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: ClusterRole
+ name: csi-azuredisk-node-role
+subjects:
+- kind: ServiceAccount
+ name: csi-azuredisk-node-sa
+ namespace: kube-system
+
+---
+
+apiVersion: apps/v1
+kind: DaemonSet
+metadata:
+ labels:
+ addon.kops.k8s.io/name: azuredisk-csi-driver.addons.k8s.io
+ app.kubernetes.io/instance: azuredisk-csi-driver
+ app.kubernetes.io/managed-by: kops
+ app.kubernetes.io/name: azuredisk-csi-driver
+ app.kubernetes.io/version: 1.34.0
+ helm.sh/chart: azuredisk-csi-driver-1.34.0
+ k8s-addon: azuredisk-csi-driver.addons.k8s.io
+ name: csi-azuredisk-node
+ namespace: kube-system
+spec:
+ selector:
+ matchLabels:
+ app: csi-azuredisk-node
+ template:
+ metadata:
+ labels:
+ app: csi-azuredisk-node
+ app.kubernetes.io/instance: azuredisk-csi-driver
+ app.kubernetes.io/managed-by: Helm
+ app.kubernetes.io/name: azuredisk-csi-driver
+ app.kubernetes.io/version: 1.34.0
+ helm.sh/chart: azuredisk-csi-driver-1.34.0
+ kops.k8s.io/managed-by: kops
+ spec:
+ affinity:
+ nodeAffinity:
+ requiredDuringSchedulingIgnoredDuringExecution:
+ nodeSelectorTerms:
+ - matchExpressions:
+ - key: type
+ operator: NotIn
+ values:
+ - virtual-kubelet
+ containers:
+ - args:
+ - --csi-address=/csi/csi.sock
+ - --probe-timeout=10s
+ - --health-port=29603
+ - --v=2
+ image: mcr.microsoft.com/oss/v2/kubernetes-csi/livenessprobe:v2.17.0
+ name: liveness-probe
+ resources:
+ limits:
+ memory: 100Mi
+ requests:
+ cpu: 10m
+ memory: 20Mi
+ securityContext:
+ capabilities:
+ drop:
+ - ALL
+ volumeMounts:
+ - mountPath: /csi
+ name: socket-dir
+ - args:
+ - --csi-address=$(ADDRESS)
+ - --kubelet-registration-path=$(DRIVER_REG_SOCK_PATH)
+ - --v=2
+ env:
+ - name: ADDRESS
+ value: /csi/csi.sock
+ - name: DRIVER_REG_SOCK_PATH
+ value: /var/lib/kubelet/plugins/disk.csi.azure.com/csi.sock
+ image: mcr.microsoft.com/oss/v2/kubernetes-csi/csi-node-driver-registrar:v2.15.0
+ name: node-driver-registrar
+ resources:
+ limits:
+ memory: 100Mi
+ requests:
+ cpu: 10m
+ memory: 20Mi
+ securityContext:
+ capabilities:
+ drop:
+ - ALL
+ volumeMounts:
+ - mountPath: /csi
+ name: socket-dir
+ - mountPath: /registration
+ name: registration-dir
+ - args:
+ - --v=5
+ - --endpoint=$(CSI_ENDPOINT)
+ - --nodeid=$(KUBE_NODE_NAME)
+ - --enable-perf-optimization=true
+ - --drivername=disk.csi.azure.com
+ - --volume-attach-limit=-1
+ - --reserved-data-disk-slot-num=0
+ - --cloud-config-secret-name=
+ - --cloud-config-secret-namespace=
+ - --custom-user-agent=
+ - --user-agent-suffix=kops
+ - --allow-empty-cloud-config=true
+ - --support-zone=true
+ - --get-node-info-from-labels=false
+ - --get-nodeid-from-imds=false
+ - --enable-otel-tracing=false
+ - --metrics-address=0.0.0.0:29605
+ - --remove-not-ready-taint=true
+ env:
+ - name: AZURE_CREDENTIAL_FILE
+ valueFrom:
+ configMapKeyRef:
+ key: path
+ name: azure-cred-file
+ optional: true
+ - name: CSI_ENDPOINT
+ value: unix:///csi/csi.sock
+ - name: KUBE_NODE_NAME
+ valueFrom:
+ fieldRef:
+ apiVersion: v1
+ fieldPath: spec.nodeName
+ - name: AZURE_GO_SDK_LOG_LEVEL
+ value: null
+ image: mcr.microsoft.com/oss/v2/kubernetes-csi/azuredisk-csi:v1.34.0
+ imagePullPolicy: IfNotPresent
+ lifecycle:
+ preStop:
+ exec:
+ command:
+ - /azurediskplugin
+ - --pre-stop-hook=true
+ livenessProbe:
+ failureThreshold: 5
+ httpGet:
+ path: /healthz
+ port: healthz
+ initialDelaySeconds: 30
+ periodSeconds: 30
+ timeoutSeconds: 30
+ name: azuredisk
+ ports:
+ - containerPort: 29603
+ name: healthz
+ protocol: TCP
+ - containerPort: 29605
+ name: metrics
+ protocol: TCP
+ resources:
+ limits:
+ memory: 1000Mi
+ requests:
+ cpu: 10m
+ memory: 20Mi
+ securityContext:
+ privileged: true
+ volumeMounts:
+ - mountPath: /csi
+ name: socket-dir
+ - mountPath: /var/lib/kubelet/
+ mountPropagation: Bidirectional
+ name: mountpoint-dir
+ - mountPath: /etc/kubernetes/
+ name: azure-cred
+ - mountPath: /dev
+ name: device-dir
+ - mountPath: /sys/bus/scsi/devices
+ name: sys-devices-dir
+ - mountPath: /sys/class/
+ name: sys-class
+ dnsPolicy: Default
+ hostNetwork: true
+ nodeSelector:
+ kubernetes.io/os: linux
+ priorityClassName: system-node-critical
+ securityContext:
+ seccompProfile:
+ type: RuntimeDefault
+ serviceAccountName: csi-azuredisk-node-sa
+ tolerations:
+ - operator: Exists
+ volumes:
+ - hostPath:
+ path: /var/lib/kubelet/plugins/disk.csi.azure.com
+ type: DirectoryOrCreate
+ name: socket-dir
+ - hostPath:
+ path: /var/lib/kubelet/
+ type: DirectoryOrCreate
+ name: mountpoint-dir
+ - hostPath:
+ path: /var/lib/kubelet/plugins_registry/
+ type: DirectoryOrCreate
+ name: registration-dir
+ - hostPath:
+ path: /etc/kubernetes/
+ type: DirectoryOrCreate
+ name: azure-cred
+ - hostPath:
+ path: /dev
+ type: Directory
+ name: device-dir
+ - hostPath:
+ path: /sys/bus/scsi/devices
+ type: Directory
+ name: sys-devices-dir
+ - hostPath:
+ path: /sys/class/
+ type: Directory
+ name: sys-class
+ updateStrategy:
+ rollingUpdate:
+ maxUnavailable: 1
+ type: RollingUpdate
+
+---
+
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ labels:
+ addon.kops.k8s.io/name: azuredisk-csi-driver.addons.k8s.io
+ app.kubernetes.io/instance: azuredisk-csi-driver
+ app.kubernetes.io/managed-by: kops
+ app.kubernetes.io/name: azuredisk-csi-driver
+ app.kubernetes.io/version: 1.34.0
+ helm.sh/chart: azuredisk-csi-driver-1.34.0
+ k8s-addon: azuredisk-csi-driver.addons.k8s.io
+ name: csi-azuredisk-controller
+ namespace: kube-system
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: csi-azuredisk-controller
+ template:
+ metadata:
+ labels:
+ app: csi-azuredisk-controller
+ app.kubernetes.io/instance: azuredisk-csi-driver
+ app.kubernetes.io/managed-by: Helm
+ app.kubernetes.io/name: azuredisk-csi-driver
+ app.kubernetes.io/version: 1.34.0
+ helm.sh/chart: azuredisk-csi-driver-1.34.0
+ kops.k8s.io/managed-by: kops
+ spec:
+ affinity:
+ nodeAffinity:
+ requiredDuringSchedulingIgnoredDuringExecution:
+ nodeSelectorTerms:
+ - matchExpressions:
+ - key: node-role.kubernetes.io/control-plane
+ operator: Exists
+ containers:
+ - args:
+ - --feature-gates=Topology=true,HonorPVReclaimPolicy=true,VolumeAttributesClass=true
+ - --csi-address=$(ADDRESS)
+ - --v=2
+ - --timeout=30s
+ - --leader-election
+ - --leader-election-namespace=kube-system
+ - --worker-threads=100
+ - --extra-create-metadata=true
+ - --strict-topology=true
+ - --kube-api-qps=50
+ - --kube-api-burst=100
+ - --retry-interval-max=30m
+ env:
+ - name: ADDRESS
+ value: /csi/csi.sock
+ image: mcr.microsoft.com/oss/v2/kubernetes-csi/csi-provisioner:v6.1.0
+ name: csi-provisioner
+ resources:
+ limits:
+ memory: 500Mi
+ requests:
+ cpu: 10m
+ memory: 20Mi
+ securityContext:
+ capabilities:
+ drop:
+ - ALL
+ volumeMounts:
+ - mountPath: /csi
+ name: socket-dir
+ - args:
+ - -v=2
+ - -csi-address=$(ADDRESS)
+ - -timeout=600s
+ - -leader-election
+ - --leader-election-namespace=kube-system
+ - -worker-threads=1000
+ - -kube-api-qps=200
+ - -kube-api-burst=400
+ env:
+ - name: ADDRESS
+ value: /csi/csi.sock
+ image: mcr.microsoft.com/oss/v2/kubernetes-csi/csi-attacher:v4.10.0
+ name: csi-attacher
+ resources:
+ limits:
+ memory: 500Mi
+ requests:
+ cpu: 10m
+ memory: 20Mi
+ securityContext:
+ capabilities:
+ drop:
+ - ALL
+ volumeMounts:
+ - mountPath: /csi
+ name: socket-dir
+ - args:
+ - -csi-address=$(ADDRESS)
+ - -leader-election
+ - --leader-election-namespace=kube-system
+ - -v=2
+ - --timeout=1200s
+ - --extra-create-metadata=true
+ - --retry-interval-max=30m
+ - --worker-threads=250
+ env:
+ - name: ADDRESS
+ value: /csi/csi.sock
+ image: mcr.microsoft.com/oss/v2/kubernetes-csi/csi-snapshotter:v8.4.0
+ name: csi-snapshotter
+ resources:
+ limits:
+ memory: 400Mi
+ requests:
+ cpu: 10m
+ memory: 20Mi
+ securityContext:
+ capabilities:
+ drop:
+ - ALL
+ volumeMounts:
+ - mountPath: /csi
+ name: socket-dir
+ - args:
+ - -csi-address=$(ADDRESS)
+ - -v=2
+ - -leader-election
+ - --leader-election-namespace=kube-system
+ - -handle-volume-inuse-error=false
+ - --feature-gates=RecoverVolumeExpansionFailure=true,VolumeAttributesClass=true
+ - -timeout=240s
+ - --retry-interval-max=30m
+ env:
+ - name: ADDRESS
+ value: /csi/csi.sock
+ image: mcr.microsoft.com/oss/v2/kubernetes-csi/csi-resizer:v2.0.0
+ name: csi-resizer
+ resources:
+ limits:
+ memory: 500Mi
+ requests:
+ cpu: 10m
+ memory: 20Mi
+ securityContext:
+ capabilities:
+ drop:
+ - ALL
+ volumeMounts:
+ - mountPath: /csi
+ name: socket-dir
+ - args:
+ - --csi-address=/csi/csi.sock
+ - --probe-timeout=3s
+ - --http-endpoint=localhost:29602
+ - --v=2
+ image: mcr.microsoft.com/oss/v2/kubernetes-csi/livenessprobe:v2.17.0
+ name: liveness-probe
+ resources:
+ limits:
+ memory: 100Mi
+ requests:
+ cpu: 10m
+ memory: 20Mi
+ securityContext:
+ capabilities:
+ drop:
+ - ALL
+ volumeMounts:
+ - mountPath: /csi
+ name: socket-dir
+ - args:
+ - --v=5
+ - --endpoint=$(CSI_ENDPOINT)
+ - --metrics-address=0.0.0.0:29604
+ - --disable-avset-nodes=true
+ - --vm-type=
+ - --drivername=disk.csi.azure.com
+ - --cloud-config-secret-name=azure-cloud-provider
+ - --cloud-config-secret-namespace=kube-system
+ - --custom-user-agent=
+ - --user-agent-suffix=kops
+ - --allow-empty-cloud-config=false
+ - --vmss-cache-ttl-seconds=60
+ - --enable-traffic-manager=false
+ - --traffic-manager-port=7788
+ - --enable-otel-tracing=false
+ - --check-disk-lun-collision=true
+ - --vmss-detach-timeout-seconds=20
+ env:
+ - name: AZURE_CREDENTIAL_FILE
+ valueFrom:
+ configMapKeyRef:
+ key: path
+ name: azure-cred-file
+ optional: true
+ - name: CSI_ENDPOINT
+ value: unix:///csi/csi.sock
+ - name: AZURE_GO_SDK_LOG_LEVEL
+ value: null
+ image: mcr.microsoft.com/oss/v2/kubernetes-csi/azuredisk-csi:v1.34.0
+ imagePullPolicy: IfNotPresent
+ livenessProbe:
+ failureThreshold: 5
+ httpGet:
+ host: localhost
+ path: /healthz
+ port: 29602
+ initialDelaySeconds: 30
+ periodSeconds: 30
+ timeoutSeconds: 10
+ name: azuredisk
+ ports:
+ - containerPort: 29604
+ name: metrics
+ protocol: TCP
+ resources:
+ limits:
+ memory: 500Mi
+ requests:
+ cpu: 10m
+ memory: 20Mi
+ securityContext:
+ capabilities:
+ drop:
+ - ALL
+ volumeMounts:
+ - mountPath: /csi
+ name: socket-dir
+ - mountPath: /etc/kubernetes/
+ name: azure-cred
+ - mountPath: /var/run/secrets/azure/tokens
+ name: azure-identity-token
+ readOnly: true
+ hostNetwork: true
+ nodeSelector:
+ kubernetes.io/os: linux
+ priorityClassName: system-cluster-critical
+ securityContext:
+ seccompProfile:
+ type: RuntimeDefault
+ serviceAccountName: csi-azuredisk-controller-sa
+ tolerations:
+ - effect: NoSchedule
+ key: node-role.kubernetes.io/master
+ operator: Exists
+ - effect: NoSchedule
+ key: node-role.kubernetes.io/controlplane
+ operator: Exists
+ - effect: NoSchedule
+ key: node-role.kubernetes.io/control-plane
+ operator: Exists
+ - effect: NoSchedule
+ key: CriticalAddonsOnly
+ operator: Exists
+ volumes:
+ - emptyDir: {}
+ name: socket-dir
+ - hostPath:
+ path: /etc/kubernetes/
+ type: DirectoryOrCreate
+ name: azure-cred
+ - name: azure-identity-token
+ projected:
+ defaultMode: 420
+ sources:
+ - serviceAccountToken:
+ audience: api://AzureADTokenExchange
+ expirationSeconds: 3600
+ path: azure-identity-token
+
+---
+
+apiVersion: storage.k8s.io/v1
+kind: CSIDriver
+metadata:
+ annotations:
+ csiDriver: v1.34.0
+ snapshot: v8.4.0
+ labels:
+ addon.kops.k8s.io/name: azuredisk-csi-driver.addons.k8s.io
+ app.kubernetes.io/managed-by: kops
+ k8s-addon: azuredisk-csi-driver.addons.k8s.io
+ name: disk.csi.azure.com
+spec:
+ attachRequired: true
+ fsGroupPolicy: File
+ podInfoOnMount: false
Integration Test: minimal_azure_wi โ Issues
-
low
The CCM cloud-config golden file has
"aadClientID": ""โ an explicitly empty string. While this is the intended WI behaviour (no client secret), it could also mask a template bug where the field is rendered but unintentionally blanked rather than omitted entirely. Azure SDK behaviour when bothuseFederatedWorkloadIdentityExtension: trueandaadClientID: ""are present is well-defined, but the empty string vs. absent field distinction is worth a comment in the source template for future maintainers. -
low
The nodes user-data golden file contains hardcoded X.509 CA certificate PEM blocks (
CACertificates) that will need to be regenerated whenever the test fixtures rotate keys, making the file fragile. This is consistent with other Azure integration tests but could cause confusion if future test runs regenerate different keys and this file is not updated. -
low
The
AZURE_GO_SDK_LOG_LEVELenv var in both the CSI node DaemonSet and controller Deployment golden files hasvalue: null(YAML null). If the template produces a literalnullstring for a missing field rather than omitting the env entry, workloads would receive the string 'null' as the log level, which may not be the intended default. This should be verified against the template rendering logic. -
low
There is no corresponding input cluster spec file (
in-v1alpha3.yamlor similar) checked in underminimal_azure_wi/in the diff. While the framework may reuse an existing input from another scenario, it would be clearer to include the input spec so reviewers can verifyuseServiceAccountExternalPermissions: trueis explicitly set in the input and not inherited from a shared fixture.
Review Complete
Key risks to keep in mind
- PR is marked do-not-merge/work-in-progress, indicating it is not yet ready for production โ may have incomplete or unstable logic.
- Large surface area (+7724/-40 across 73 files) increases review complexity and the chance of subtle bugs.
- New Azure infrastructure tasks (ManagedIdentity, FederatedIdentityCredential, RoleAssignment changes) could misconfigure RBAC or identity bindings if logic is incorrect, leading to privilege escalation or cluster authentication failures.
- Vendor dependency on a new Azure SDK package (armmsi) adds supply-chain risk and must be audited.
- Changes to cluster.go API types and generated conversion code (v1alpha2/v1alpha3) could introduce breaking changes or conversion bugs for existing clusters.
- Modifications to apply_cluster.go and template_functions.go are central codepaths that affect all Azure cluster operations, not just WI-enabled ones.
- OIDC/issuer discovery blob is now written to Azure Blob Storage; misconfigured permissions on that blob could expose federation metadata or allow spoofing.
- Integration test golden files are generated but the underlying logic has not yet been proven stable (WIP label).