Skip to content

Commit 1a19654

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 1a19654

31 files changed

Lines changed: 549 additions & 91 deletions

File tree

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`.

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

0 commit comments

Comments
 (0)