Skip to content

Commit bc8e90b

Browse files
Merge pull request #18661 from hakman/hakman-aws-s3-nodeup-download
aws: download nodeup from private S3 buckets with curl
2 parents 362646c + 4a1e07d commit bc8e90b

19 files changed

Lines changed: 596 additions & 75 deletions

File tree

docs/operations/asset-repository.md

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,11 @@ spec:
4141
fileRepository: https://example.com/files
4242
```
4343

44-
The repository must allow nodes to perform unauthenticated reads. The repository can
45-
be public or it can allow read access through network connectivity, such as access
46-
through a particular AWS Endpoint.
44+
For an `http://` or `https://` repository, nodes must be able to read without credentials.
45+
The repository can be public or allow access through network connectivity, such as a
46+
particular cloud endpoint.
47+
48+
{{ kops_feature_table(kops_added_default='1.37') }}
4749

4850
On GCE, the repository can also be a `gs://` URL. Nodes then read it with the credentials of
4951
their service account, which allows the bucket to be private. The service accounts of the
@@ -55,6 +57,20 @@ spec:
5557
fileRepository: gs://example-bucket/files
5658
```
5759

60+
{{ kops_feature_table(kops_added_default='1.37') }}
61+
62+
On AWS, the repository can also be an `s3://` URL. Nodes then read it with the credentials of
63+
their instance profile, which allows the bucket to be private. The instance profiles of the
64+
instance groups have to be granted `s3:GetObject` on that bucket. This is supported in the
65+
commercial AWS and AWS GovCloud partitions. Downloading nodeup itself from an `s3://`
66+
`KOPS_BASE_URL` additionally requires curl 8.0 or newer on the node image.
67+
68+
```yaml
69+
spec:
70+
assets:
71+
fileRepository: s3://example-bucket/files
72+
```
73+
5874
## Copying assets into repositories
5975

6076
{{ kops_feature_table(kops_added_default='1.22') }}
@@ -65,7 +81,7 @@ When running `kops get assets --copy`, kOps copies assets into their respective
6581
they do not already exist there.
6682

6783
For file assets, kOps only supports copying to a repository that is either an S3 or GCS bucket.
68-
An S3 bucket must be configured using the [regional naming conventions of S3](https://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region).
84+
An S3 bucket must be configured with a prefix of `s3://` or using the [regional naming conventions of S3](https://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region).
6985
A GCS bucket must be configured with a prefix of `https://storage.googleapis.com/` or `gs://`.
7086

7187
## Listing assets

docs/releases/1.37-NOTES.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ As part of this removal, the `protokube` component, whose only remaining respons
3232

3333
* On AWS, instance roles that require no permissions, such as the bastion role and the default worker node role, no longer have an inline IAM policy. `kops update cluster` deletes the previously created inline policy from such roles. With the Terraform target, the corresponding `aws_iam_role_policy` resources are removed from the configuration and destroyed on the next apply.
3434

35+
* Private cluster asset repositories now support AWS `s3://` URLs in addition to existing GCE `gs://` URLs, both for `KOPS_BASE_URL` (nodeup download) and `spec.assets.fileRepository`. Nodes authenticate with their instance credentials; see the [asset repository documentation](https://kops.sigs.k8s.io/operations/asset-repository/) for the required permissions. The AWS nodeup download requires node images with curl 8.0 or newer.
36+
3537
# Breaking changes
3638

3739
* Support for AWS Classic Load Balancer (CLB) for the API has been removed. Clusters with `spec.api.loadBalancer.class: Classic` (or with no explicit `class`, which previously defaulted to Classic) fail validation, and the long-deprecated `kops create cluster --api-loadbalancer-class` flag has been removed. Existing clusters using a CLB must migrate to a Network Load Balancer (NLB) using kOps 1.36 or earlier before upgrading to kOps 1.37, following the [CLB to NLB migration guide](https://github.com/kubernetes/kops/blob/master/permalinks/acm_nlb.md). Attaching instance groups to externally-managed Classic Load Balancers via `spec.externalLoadBalancers[].loadBalancerName` remains supported.

hack/dev-build-aws.sh

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@
1414
# See the License for the specific language governing permissions and
1515
# limitations under the License.
1616

17-
# This is a convenience script for developing kOps on GCE.
18-
# It builds the code, including nodeup, and uploads to a custom GCS bucket.
17+
# This is a convenience script for developing kOps on AWS.
18+
# It builds the code, including nodeup, and uploads to a custom S3 bucket.
1919
# It also sets KOPS_STATE_STORE and KOPS_BASE_URL to project-isolated values.
20-
# To use, source the script. For example `. hack/dev-build-gce.sh` (note the initial `.`)
20+
# To use, source the script. For example, `. hack/dev-build-aws.sh` (note the initial `.`).
2121

2222
# Can't use set -e in a script we want to source
2323
#set -e
@@ -37,9 +37,11 @@ aws s3 ls "${UPLOAD_DEST}" || aws s3 mb "${UPLOAD_DEST}" || return
3737
make kops-install dev-upload-linux-${KOPS_ARCH} || return
3838

3939
# Set KOPS_BASE_URL
40-
(tools/get_version.sh | grep VERSION | awk '{print $2}') || return
41-
KOPS_VERSION=$(tools/get_version.sh | grep VERSION | awk '{print $2}')
42-
export KOPS_BASE_URL=https://${S3_BUCKET_NAME}.s3.amazonaws.com/kops/${KOPS_VERSION}/
40+
KOPS_VERSION=$(tools/get_version.sh | grep VERSION | awk '{print $2}') || return
41+
echo "${KOPS_VERSION}"
42+
# The s3:// form lets nodes use their instance profiles; https:// requires public read access.
43+
# Grant the cluster instance profiles s3:GetObject on the bucket and use node images with curl >= 8.0.
44+
export KOPS_BASE_URL=s3://${S3_BUCKET_NAME}/kops/${KOPS_VERSION}/
4345

4446
# Create the state-store bucket if it doesn't exist
4547
KOPS_STATE_STORE="s3://kops-state-${ACCOUNT_ID}-${USER}"

pkg/apis/kops/validation/aws.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ limitations under the License.
1717
package validation
1818

1919
import (
20+
"context"
2021
"fmt"
2122
"net"
2223
"strconv"
@@ -63,13 +64,38 @@ func awsValidateCluster(c *kops.Cluster, strict bool) field.ErrorList {
6364

6465
allErrs = append(allErrs, awsValidateUseIPBasedNodeNames(c)...)
6566

67+
allErrs = append(allErrs, awsValidateS3FileRepositoryPartition(c)...)
68+
6669
if c.Spec.Authentication != nil && c.Spec.Authentication.AWS != nil {
6770
allErrs = append(allErrs, awsValidateIAMAuthenticator(field.NewPath("spec", "authentication", "aws"), c.Spec.Authentication.AWS)...)
6871
}
6972

7073
return allErrs
7174
}
7275

76+
func awsValidateS3FileRepositoryPartition(cluster *kops.Cluster) field.ErrorList {
77+
assets := cluster.Spec.Assets
78+
if assets == nil || assets.FileRepository == nil || !strings.HasPrefix(*assets.FileRepository, "s3://") {
79+
return nil
80+
}
81+
82+
region, err := awsup.FindRegion(cluster)
83+
if err != nil {
84+
// Subnet validation reports invalid or missing zones separately.
85+
return nil
86+
}
87+
88+
fldPath := field.NewPath("spec", "assets", "fileRepository")
89+
supported, err := awsup.SupportsS3BootstrapEndpoint(context.TODO(), region)
90+
if err != nil {
91+
return field.ErrorList{field.Invalid(fldPath, *assets.FileRepository, err.Error())}
92+
}
93+
if !supported {
94+
return field.ErrorList{field.Forbidden(fldPath, fmt.Sprintf("s3:// fileRepository is not supported in AWS region %q", region))}
95+
}
96+
return nil
97+
}
98+
7399
func awsValidateEBSCSIDriver(cluster *kops.Cluster) (allErrs field.ErrorList) {
74100
c := cluster.Spec
75101

pkg/apis/kops/validation/aws_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -923,6 +923,59 @@ func TestAWSAdditionalRoutes(t *testing.T) {
923923
}
924924
}
925925

926+
func TestAWSValidateS3FileRepository(t *testing.T) {
927+
for _, tc := range []struct {
928+
name string
929+
region string
930+
fileRepository string
931+
expectedErrors []string
932+
}{
933+
{
934+
name: "commercial partition",
935+
region: "us-east-1",
936+
fileRepository: "s3://example-k8s-assets/kops",
937+
},
938+
{
939+
name: "GovCloud partition",
940+
region: "us-gov-west-1",
941+
fileRepository: "s3://example-k8s-assets/kops",
942+
},
943+
{
944+
name: "China partition",
945+
region: "cn-north-1",
946+
fileRepository: "s3://example-k8s-assets/kops",
947+
expectedErrors: []string{"Forbidden::spec.assets.fileRepository"},
948+
},
949+
{
950+
name: "ISO partition",
951+
region: "us-iso-east-1",
952+
fileRepository: "s3://example-k8s-assets/kops",
953+
expectedErrors: []string{"Forbidden::spec.assets.fileRepository"},
954+
},
955+
{
956+
name: "HTTPS repository in ISO partition",
957+
region: "us-iso-east-1",
958+
fileRepository: "https://example.com/kops",
959+
},
960+
} {
961+
t.Run(tc.name, func(t *testing.T) {
962+
fileRepository := tc.fileRepository
963+
cluster := &kops.Cluster{
964+
Spec: kops.ClusterSpec{
965+
CloudProvider: kops.CloudProviderSpec{AWS: &kops.AWSSpec{}},
966+
Assets: &kops.AssetsSpec{FileRepository: &fileRepository},
967+
Networking: kops.NetworkingSpec{
968+
Subnets: []kops.ClusterSubnetSpec{{Name: "subnet", Zone: tc.region + "a"}},
969+
},
970+
},
971+
}
972+
973+
errs := awsValidateCluster(cluster, false)
974+
testErrors(t, tc.name, errs, tc.expectedErrors)
975+
})
976+
}
977+
}
978+
926979
func TestAWSValidateNLBSecurityGroupMode(t *testing.T) {
927980
grid := []struct {
928981
Input kops.ClusterSpec

pkg/apis/kops/validation/validation.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -801,8 +801,13 @@ func validateFileRepository(s string, fieldPath *field.Path, cloudProvider kops.
801801
if cloudProvider != kops.CloudProviderGCE {
802802
allErrs = append(allErrs, field.Invalid(fieldPath, s, fmt.Sprintf("gs:// fileRepository is only supported on GCE, but the cloud provider is %q", cloudProvider)))
803803
}
804+
case "s3":
805+
// Only AWS instances can authenticate to S3 with their instance profile.
806+
if cloudProvider != kops.CloudProviderAWS {
807+
allErrs = append(allErrs, field.Invalid(fieldPath, s, fmt.Sprintf("s3:// fileRepository is only supported on AWS, but the cloud provider is %q", cloudProvider)))
808+
}
804809
default:
805-
allErrs = append(allErrs, field.Invalid(fieldPath, s, "fileRepository must be an http:// or https:// URL"))
810+
allErrs = append(allErrs, field.Invalid(fieldPath, s, "fileRepository must be an http://, https://, gs://, or s3:// URL"))
806811
}
807812
if u.Host == "" {
808813
allErrs = append(allErrs, field.Invalid(fieldPath, s, "fileRepository must include a host"))

pkg/apis/kops/validation/validation_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2292,6 +2292,15 @@ func TestValidateFileRepository(t *testing.T) {
22922292
{
22932293
Input: "http://example.com/files",
22942294
},
2295+
{
2296+
Input: "s3://example-k8s-assets/kops",
2297+
CloudProvider: kops.CloudProviderAWS,
2298+
},
2299+
{
2300+
Input: "s3://example-k8s-assets/kops",
2301+
CloudProvider: kops.CloudProviderGCE,
2302+
ExpectedErrors: []string{"Invalid value::spec.assets.fileRepository"},
2303+
},
22952304
{
22962305
Input: "s3://example-k8s-assets/kops",
22972306
ExpectedErrors: []string{"Invalid value::spec.assets.fileRepository"},

pkg/assets/assetcopy/copyfile.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -176,11 +176,11 @@ func writeFile(ctx context.Context, cluster *kops.Cluster, p vfs.Path, data []by
176176
return nil
177177
}
178178

179-
// buildVFSPath task a recognizable https url and transforms that URL into the equivalent url with the object
180-
// store prefix.
179+
// buildVFSPath returns local paths and memfs://, file://, gs://, and s3:// URLs unchanged.
180+
// It converts recognized S3 or GCS HTTPS URLs to their native VFS form.
181181
func buildVFSPath(target string) (string, error) {
182182
if !strings.Contains(target, "://") || strings.HasPrefix(target, "memfs://") || strings.HasPrefix(target, "file://") ||
183-
strings.HasPrefix(target, "gs://") {
183+
strings.HasPrefix(target, "gs://") || strings.HasPrefix(target, "s3://") {
184184
return target, nil
185185
}
186186

@@ -207,7 +207,7 @@ func buildVFSPath(target string) (string, error) {
207207
if vfsPath == "" {
208208
klog.Errorf("Unable to determine VFS path from supplied URL: %s", target)
209209
klog.Errorf("S3, Google Cloud Storage, and File Paths are supported.")
210-
klog.Errorf("For S3, please make sure that the supplied file repository URL adhere to S3 naming conventions, https://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region.")
210+
klog.Errorf("For S3, please make sure that the supplied file repository URL starts with s3:// or adheres to S3 naming conventions.")
211211
klog.Errorf("For GCS, please make sure that the supplied file repository URL starts with gs:// or https://storage.googleapis.com/")
212212
if err != nil { // print the S3 error for more details
213213
return "", fmt.Errorf("Error Details: %v", err)

pkg/assets/assetcopy/copyfile_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ func Test_BuildVFSPath(t *testing.T) {
5151
"s3://k8s-for-greeks-kops/kubernetes-release/release/v1.7.2/bin/linux/amd64/kubectl",
5252
true,
5353
},
54+
{
55+
"s3://k8s-for-greeks-kops/kubernetes-release/release/v1.7.2/bin/linux/amd64/kubectl",
56+
"s3://k8s-for-greeks-kops/kubernetes-release/release/v1.7.2/bin/linux/amd64/kubectl",
57+
true,
58+
},
5459
{
5560
"https://foo/k8s-for-greeks-kops/kubernetes-release/release/v1.7.2/bin/linux/amd64/kubectl",
5661
"",

pkg/assets/builder.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,8 @@ func (a *AssetBuilder) remapURL(canonicalURL *url.URL) (*url.URL, error) {
442442
}
443443

444444
fileRepo.Path = path.Join(fileRepo.Path, canonicalURL.Path)
445+
// Escape commas, which are legal in a path but separate locations in CompactString.
446+
fileRepo.RawPath = strings.ReplaceAll(fileRepo.EscapedPath(), ",", "%2C")
445447

446448
return fileRepo, nil
447449
}

0 commit comments

Comments
 (0)