Skip to content

Commit a5d6563

Browse files
committed
azure: encode storage account in azureblob:// URLs
Move the Azure storage account name from the AZURE_STORAGE_ACCOUNT env var into the URL so each azureblob:// path is self-contained. New format: azureblob://{account}/{container}/{key}. The env var is no longer read by kops; the legacy URL form is rejected with a migration error pointing users at the new shape. Signed-off-by: Ciprian Hacman <ciprian@hakman.dev>
1 parent 7507065 commit a5d6563

34 files changed

Lines changed: 550 additions & 93 deletions

File tree

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ UPLOAD_CMD=$(KOPS_ROOT)/hack/upload ${UPLOAD_ARGS}
5050
unexport AWS_ACCESS_KEY_ID AWS_REGION AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN CNI_VERSION_URL DNS_IGNORE_NS_CHECK DNSCONTROLLER_IMAGE DO_ACCESS_TOKEN GOOGLE_APPLICATION_CREDENTIALS
5151
unexport KOPS_BASE_URL KOPS_CLUSTER_NAME KOPS_RUN_OBSOLETE_VERSION KOPS_STATE_STORE KOPS_STATE_S3_ACL KUBE_API_VERSIONS NODEUP_URL OPENSTACK_CREDENTIAL_FILE SKIP_PACKAGE_UPDATE
5252
unexport SKIP_REGION_CHECK S3_ACCESS_KEY_ID S3_ENDPOINT S3_REGION S3_SECRET_ACCESS_KEY HCLOUD_TOKEN SCW_ACCESS_KEY SCW_SECRET_KEY SCW_DEFAULT_PROJECT_ID SCW_PROFILE
53-
unexport AZURE_CLIENT_ID AZURE_CLIENT_SECRET AZURE_STORAGE_ACCOUNT AZURE_SUBSCRIPTION_ID AZURE_TENANT_ID
53+
unexport AZURE_CLIENT_ID AZURE_CLIENT_SECRET AZURE_SUBSCRIPTION_ID AZURE_TENANT_ID
5454

5555

5656
VERSION=$(shell tools/get_version.sh | grep VERSION | awk '{print $$2}')

cmd/kops/create_cluster_integration_test.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@ func TestCreateClusterGossipAWS(t *testing.T) {
5151

5252
// TestCreateClusterGossipAzure creates a minimal Azure gossip cluster
5353
func TestCreateClusterGossipAzure(t *testing.T) {
54-
t.Setenv("AZURE_STORAGE_ACCOUNT", "teststorage")
5554
runCreateClusterIntegrationTest(t, "../../tests/integration/create_cluster/gossip-azure", "v1alpha2")
5655
}
5756

cmd/kops/integration_test.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1792,7 +1792,6 @@ func (i *integrationTest) runTestTerraformGCE(t *testing.T) {
17921792
}
17931793

17941794
func (i *integrationTest) runTestTerraformAzure(t *testing.T) {
1795-
t.Setenv("AZURE_STORAGE_ACCOUNT", "teststorage")
17961795
t.Setenv("KOPS_RUN_TOO_NEW_VERSION", "1")
17971796

17981797
featureflag.ParseFlags("+Azure,+AzureTerraform")

docs/getting_started/azure.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,12 @@ export KOPS_FEATURE_FLAGS="Azure"
3030

3131
```bash
3232
export AZURE_SUBSCRIPTION_ID=<subscription-id>
33-
export AZURE_STORAGE_ACCOUNT=<storage-account-name>
3433
```
3534

3635
### kOps-specific
3736

3837
```bash
39-
export KOPS_STATE_STORE=azureblob://<container-name>
38+
export KOPS_STATE_STORE=azureblob://<storage-account-name>/<container-name>
4039
```
4140

4241
## Creating a Single Master Cluster
@@ -90,3 +89,11 @@ kOps for Azure currently does not support the following features:
9089
## Next steps
9190

9291
Now that you have a working kOps cluster, read through the recommendations for [production setups guide](production.md) to learn more about how to configure kOps for production workloads.
92+
93+
## Migrating from earlier alpha versions
94+
95+
Older alpha releases used `azureblob://<container>/...` URLs and read the storage account from `AZURE_STORAGE_ACCOUNT`. To upgrade an existing cluster:
96+
97+
1. `unset AZURE_STORAGE_ACCOUNT` and re-export `KOPS_STATE_STORE` in the new shape.
98+
2. `kops edit cluster` to update `spec.configStore.base` to the updated URL.
99+
3. `kops update cluster --yes` and `kops rolling-update cluster --yes`.

hack/update-expected.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ unset AWS_ACCESS_KEY_ID AWS_REGION AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN CNI_V
3737
unset KOPS_CLUSTER_NAME KOPS_RUN_OBSOLETE_VERSION KOPS_STATE_STORE KOPS_STATE_S3_ACL KUBE_API_VERSIONS NODEUP_URL OPENSTACK_CREDENTIAL_FILE PROTOKUBE_IMAGE SKIP_PACKAGE_UPDATE
3838
unset SKIP_REGION_CHECK S3_ACCESS_KEY_ID S3_ENDPOINT S3_REGION S3_SECRET_ACCESS_KEY
3939
unset SCW_ACCESS_KEY SCW_SECRET_KEY SCW_DEFAULT_PROJECT_ID SCW_PROFILE
40-
unset AZURE_CLIENT_ID AZURE_CLIENT_SECRET AZURE_STORAGE_ACCOUNT AZURE_SUBSCRIPTION_ID AZURE_TENANT_ID
40+
unset AZURE_CLIENT_ID AZURE_CLIENT_SECRET AZURE_SUBSCRIPTION_ID AZURE_TENANT_ID
4141
unset DIGITALOCEAN_ACCESS_TOKEN
4242

4343
# Run the tests in "autofix mode"

nodeup/pkg/bootstrap/install.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -127,10 +127,6 @@ func (i *Installation) buildEnvFile() *nodetasks.InstallFile {
127127
envVars["OSS_REGION"] = os.Getenv("OSS_REGION")
128128
}
129129

130-
if os.Getenv("AZURE_STORAGE_ACCOUNT") != "" {
131-
envVars["AZURE_STORAGE_ACCOUNT"] = os.Getenv("AZURE_STORAGE_ACCOUNT")
132-
}
133-
134130
if os.Getenv("SCW_PROFILE") != "" || os.Getenv("SCW_SECRET_KEY") != "" {
135131
profile, err := scaleway.CreateValidScalewayProfile()
136132
if err != nil {

nodeup/pkg/model/protokube.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -287,10 +287,6 @@ func (t *ProtokubeBuilder) buildEnvFile() (*nodetasks.File, error) {
287287
envVars["OSS_REGION"] = os.Getenv("OSS_REGION")
288288
}
289289

290-
if os.Getenv("AZURE_STORAGE_ACCOUNT") != "" {
291-
envVars["AZURE_STORAGE_ACCOUNT"] = os.Getenv("AZURE_STORAGE_ACCOUNT")
292-
}
293-
294290
if t.CloudProvider() == kops.CloudProviderScaleway {
295291
if os.Getenv("SCW_PROFILE") != "" || os.Getenv("SCW_SECRET_KEY") != "" {
296292
profile, err := scaleway.CreateValidScalewayProfile()

pkg/apis/kops/validation/validation.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import (
4545
"k8s.io/kops/pkg/model/iam"
4646
"k8s.io/kops/upup/pkg/fi"
4747
"k8s.io/kops/upup/pkg/fi/utils"
48+
"k8s.io/kops/util/pkg/vfs"
4849
)
4950

5051
func newValidateCluster(cluster *kops.Cluster, strict bool) field.ErrorList {
@@ -199,6 +200,8 @@ func validateClusterSpec(spec *kops.ClusterSpec, c *kops.Cluster, fieldPath *fie
199200
}
200201
}
201202

203+
allErrs = append(allErrs, validateAzureBlobAccountUniformity(spec, fieldPath)...)
204+
202205
if spec.ContainerRuntime != "" {
203206
allErrs = append(allErrs, validateContainerRuntime(c, spec.ContainerRuntime, fieldPath.Child("containerRuntime"))...)
204207
}
@@ -1521,6 +1524,82 @@ func validateEtcdBackupStore(specs []kops.EtcdClusterSpec, fieldPath *field.Path
15211524
return allErrs
15221525
}
15231526

1527+
// azureBlobAccount returns the storage account encoded in an azureblob:// URL,
1528+
// or "" with no error if the URL is not azureblob://. Returns an error only if
1529+
// the URL has the azureblob:// prefix but fails to parse.
1530+
func azureBlobAccount(rawURL string) (string, error) {
1531+
if !strings.HasPrefix(rawURL, "azureblob://") {
1532+
return "", nil
1533+
}
1534+
p, err := vfs.Context.BuildVfsPath(rawURL)
1535+
if err != nil {
1536+
return "", err
1537+
}
1538+
azPath, ok := p.(*vfs.AzureBlobPath)
1539+
if !ok {
1540+
return "", fmt.Errorf("expected azureblob:// URL, got %q", rawURL)
1541+
}
1542+
return azPath.Account(), nil
1543+
}
1544+
1545+
// validateAzureBlobAccountUniformity enforces that every azureblob:// URL in
1546+
// the cluster spec uses the same storage account as configStore.base. Any
1547+
// azureblob:// URL elsewhere in the spec is rejected when configStore.base is
1548+
// not itself azureblob://.
1549+
func validateAzureBlobAccountUniformity(spec *kops.ClusterSpec, fieldPath *field.Path) field.ErrorList {
1550+
var allErrs field.ErrorList
1551+
csPath := fieldPath.Child("configStore")
1552+
1553+
canonical := ""
1554+
if strings.HasPrefix(spec.ConfigStore.Base, "azureblob://") {
1555+
account, err := azureBlobAccount(spec.ConfigStore.Base)
1556+
if err != nil {
1557+
allErrs = append(allErrs, field.Invalid(csPath.Child("base"), spec.ConfigStore.Base, err.Error()))
1558+
return allErrs
1559+
}
1560+
canonical = account
1561+
}
1562+
1563+
type entry struct {
1564+
path *field.Path
1565+
url string
1566+
}
1567+
others := []entry{
1568+
{csPath.Child("keypairs"), spec.ConfigStore.Keypairs},
1569+
{csPath.Child("secrets"), spec.ConfigStore.Secrets},
1570+
}
1571+
for i, ec := range spec.EtcdClusters {
1572+
if ec.Backups != nil {
1573+
others = append(others, entry{
1574+
fieldPath.Child("etcdClusters").Index(i).Child("backups", "backupStore"),
1575+
ec.Backups.BackupStore,
1576+
})
1577+
}
1578+
}
1579+
1580+
for _, e := range others {
1581+
if !strings.HasPrefix(e.url, "azureblob://") {
1582+
continue
1583+
}
1584+
account, err := azureBlobAccount(e.url)
1585+
if err != nil {
1586+
allErrs = append(allErrs, field.Invalid(e.path, e.url, err.Error()))
1587+
continue
1588+
}
1589+
if canonical == "" {
1590+
allErrs = append(allErrs, field.Invalid(e.path, e.url,
1591+
"azureblob:// URL requires configStore.base to also be azureblob://"))
1592+
continue
1593+
}
1594+
if account != canonical {
1595+
allErrs = append(allErrs, field.Invalid(e.path, e.url,
1596+
fmt.Sprintf("storage account %q does not match configStore.base account %q", account, canonical)))
1597+
}
1598+
}
1599+
1600+
return allErrs
1601+
}
1602+
15241603
// validateEtcdStorage is responsible for checking versions are identical.
15251604
func validateEtcdStorage(specs []kops.EtcdClusterSpec, fieldPath *field.Path) field.ErrorList {
15261605
allErrs := field.ErrorList{}

pkg/apis/kops/validation/validation_test.go

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1931,3 +1931,158 @@ func TestValidateNetworkingLinode(t *testing.T) {
19311931
})
19321932
}
19331933
}
1934+
1935+
func TestValidateAzureBlobAccountUniformity(t *testing.T) {
1936+
tests := []struct {
1937+
name string
1938+
spec kops.ClusterSpec
1939+
expected []*field.Error
1940+
}{
1941+
{
1942+
name: "all matching azureblob URLs",
1943+
spec: kops.ClusterSpec{
1944+
ConfigStore: kops.ConfigStoreSpec{
1945+
Base: "azureblob://kopsstate/state/cluster.example.com",
1946+
Keypairs: "azureblob://kopsstate/state/cluster.example.com/pki",
1947+
Secrets: "azureblob://kopsstate/state/cluster.example.com/secrets",
1948+
},
1949+
EtcdClusters: []kops.EtcdClusterSpec{{
1950+
Backups: &kops.EtcdBackupSpec{
1951+
BackupStore: "azureblob://kopsstate/state/cluster.example.com/backups/etcd/main",
1952+
},
1953+
}},
1954+
},
1955+
},
1956+
{
1957+
name: "non-azure cluster is unaffected",
1958+
spec: kops.ClusterSpec{
1959+
ConfigStore: kops.ConfigStoreSpec{
1960+
Base: "s3://my-bucket/cluster.example.com",
1961+
Keypairs: "s3://my-bucket/cluster.example.com/pki",
1962+
},
1963+
EtcdClusters: []kops.EtcdClusterSpec{{
1964+
Backups: &kops.EtcdBackupSpec{
1965+
BackupStore: "s3://my-bucket/cluster.example.com/backups/etcd/main",
1966+
},
1967+
}},
1968+
},
1969+
},
1970+
{
1971+
name: "keypairs uses different storage account",
1972+
spec: kops.ClusterSpec{
1973+
ConfigStore: kops.ConfigStoreSpec{
1974+
Base: "azureblob://kopsstate/state/cluster.example.com",
1975+
Keypairs: "azureblob://otheracct/state/cluster.example.com/pki",
1976+
},
1977+
},
1978+
expected: []*field.Error{
1979+
{
1980+
Type: field.ErrorTypeInvalid,
1981+
Field: "spec.configStore.keypairs",
1982+
},
1983+
},
1984+
},
1985+
{
1986+
name: "secrets uses different storage account",
1987+
spec: kops.ClusterSpec{
1988+
ConfigStore: kops.ConfigStoreSpec{
1989+
Base: "azureblob://kopsstate/state/cluster.example.com",
1990+
Secrets: "azureblob://otheracct/state/cluster.example.com/secrets",
1991+
},
1992+
},
1993+
expected: []*field.Error{
1994+
{
1995+
Type: field.ErrorTypeInvalid,
1996+
Field: "spec.configStore.secrets",
1997+
},
1998+
},
1999+
},
2000+
{
2001+
name: "etcd backupStore uses different storage account",
2002+
spec: kops.ClusterSpec{
2003+
ConfigStore: kops.ConfigStoreSpec{
2004+
Base: "azureblob://kopsstate/state/cluster.example.com",
2005+
},
2006+
EtcdClusters: []kops.EtcdClusterSpec{{
2007+
Backups: &kops.EtcdBackupSpec{
2008+
BackupStore: "azureblob://otheracct/backups/etcd/main",
2009+
},
2010+
}},
2011+
},
2012+
expected: []*field.Error{
2013+
{
2014+
Type: field.ErrorTypeInvalid,
2015+
Field: "spec.etcdClusters[0].backups.backupStore",
2016+
},
2017+
},
2018+
},
2019+
{
2020+
name: "azureblob backupStore with non-azure configStore.base is rejected",
2021+
spec: kops.ClusterSpec{
2022+
ConfigStore: kops.ConfigStoreSpec{
2023+
Base: "s3://my-bucket/cluster.example.com",
2024+
},
2025+
EtcdClusters: []kops.EtcdClusterSpec{{
2026+
Backups: &kops.EtcdBackupSpec{
2027+
BackupStore: "azureblob://kopsstate/backups/etcd/main",
2028+
},
2029+
}},
2030+
},
2031+
expected: []*field.Error{
2032+
{
2033+
Type: field.ErrorTypeInvalid,
2034+
Field: "spec.etcdClusters[0].backups.backupStore",
2035+
},
2036+
},
2037+
},
2038+
{
2039+
name: "malformed azureblob configStore.base is rejected",
2040+
spec: kops.ClusterSpec{
2041+
ConfigStore: kops.ConfigStoreSpec{
2042+
Base: "azureblob://kopsstate",
2043+
},
2044+
},
2045+
expected: []*field.Error{
2046+
{
2047+
Type: field.ErrorTypeInvalid,
2048+
Field: "spec.configStore.base",
2049+
},
2050+
},
2051+
},
2052+
{
2053+
name: "malformed azureblob keypairs is rejected",
2054+
spec: kops.ClusterSpec{
2055+
ConfigStore: kops.ConfigStoreSpec{
2056+
Base: "azureblob://kopsstate/state/cluster.example.com",
2057+
Keypairs: "azureblob://kopsstate",
2058+
},
2059+
},
2060+
expected: []*field.Error{
2061+
{
2062+
Type: field.ErrorTypeInvalid,
2063+
Field: "spec.configStore.keypairs",
2064+
},
2065+
},
2066+
},
2067+
{
2068+
name: "non-azure backup store with azure config base is allowed",
2069+
spec: kops.ClusterSpec{
2070+
ConfigStore: kops.ConfigStoreSpec{
2071+
Base: "azureblob://kopsstate/state/cluster.example.com",
2072+
},
2073+
EtcdClusters: []kops.EtcdClusterSpec{{
2074+
Backups: &kops.EtcdBackupSpec{
2075+
BackupStore: "memfs://tests/cluster.example.com/backups/etcd/main",
2076+
},
2077+
}},
2078+
},
2079+
},
2080+
}
2081+
2082+
for _, tt := range tests {
2083+
t.Run(tt.name, func(t *testing.T) {
2084+
errList := validateAzureBlobAccountUniformity(&tt.spec, field.NewPath("spec"))
2085+
testFieldErrors(t, errList, tt.expected)
2086+
})
2087+
}
2088+
}

0 commit comments

Comments
 (0)