From e88c6f5de4c26651c9571d74c81720c8b7dc141d Mon Sep 17 00:00:00 2001 From: bjuncosa Date: Thu, 16 Mar 2017 12:39:40 +0100 Subject: [PATCH] Add feature: Custom IAM Instance Profile This way Cluster IAM roles can be managed externally, either manually, using cloudformation or any other tool. --- cmd/kops/integration_test.go | 61 +-- docs/cluster_spec.md | 17 + docs/iam_roles.md | 31 +- pkg/apis/kops/cluster.go | 27 ++ pkg/apis/kops/v1alpha1/cluster.go | 26 ++ .../kops/v1alpha1/zz_generated.conversion.go | 155 +++++--- pkg/apis/kops/v1alpha2/cluster.go | 26 ++ .../kops/v1alpha2/zz_generated.conversion.go | 155 +++++--- pkg/apis/kops/validation/validation.go | 30 ++ pkg/apis/kops/validation/validation_test.go | 62 ++- pkg/featureflag/featureflag.go | 3 + pkg/model/awsmodel/autoscalinggroup.go | 7 +- pkg/model/iam.go | 96 ++++- pkg/model/names.go | 56 ++- tests/integration/custom_iam_role/id_rsa.pub | 1 + .../custom_iam_role/in-v1alpha2.yaml | 82 ++++ .../integration/custom_iam_role/kubernetes.tf | 352 ++++++++++++++++++ .../fi/cloudup/awstasks/iaminstanceprofile.go | 30 +- .../awstasks/iaminstanceprofilerole.go | 6 + upup/pkg/fi/cloudup/awstasks/iamrole.go | 21 +- 20 files changed, 1081 insertions(+), 163 deletions(-) create mode 100755 tests/integration/custom_iam_role/id_rsa.pub create mode 100644 tests/integration/custom_iam_role/in-v1alpha2.yaml create mode 100644 tests/integration/custom_iam_role/kubernetes.tf diff --git a/cmd/kops/integration_test.go b/cmd/kops/integration_test.go index 56b251ae55ab5..5611b53d5b2fa 100644 --- a/cmd/kops/integration_test.go +++ b/cmd/kops/integration_test.go @@ -47,26 +47,24 @@ import ( // TestMinimal runs the test on a minimum configuration, similar to kops create cluster minimal.example.com --zones us-west-1a func TestMinimal(t *testing.T) { - runTestAWS(t, "minimal.example.com", "../../tests/integration/minimal", "v1alpha0", false, 1) - runTestAWS(t, "minimal.example.com", "../../tests/integration/minimal", "v1alpha1", false, 1) - runTestAWS(t, "minimal.example.com", "../../tests/integration/minimal", "v1alpha2", false, 1) + runTestAWS(t, "minimal.example.com", "../../tests/integration/minimal", "v1alpha0", false, 1, true) + runTestAWS(t, "minimal.example.com", "../../tests/integration/minimal", "v1alpha1", false, 1, true) + runTestAWS(t, "minimal.example.com", "../../tests/integration/minimal", "v1alpha2", false, 1, true) } // TestHA runs the test on a simple HA configuration, similar to kops create cluster minimal.example.com --zones us-west-1a,us-west-1b,us-west-1c --master-count=3 func TestHA(t *testing.T) { - runTestAWS(t, "ha.example.com", "../../tests/integration/ha", "v1alpha1", false, 3) - runTestAWS(t, "ha.example.com", "../../tests/integration/ha", "v1alpha2", false, 3) + runTestAWS(t, "ha.example.com", "../../tests/integration/ha", "v1alpha1", false, 3, true) + runTestAWS(t, "ha.example.com", "../../tests/integration/ha", "v1alpha2", false, 3, true) } -// TestHighAvailabilityGCE runs the test on a simple HA GCE configuration, similar to kops create cluster ha-gce.example.com -// --zones us-test1-a,us-test1-b,us-test1-c --master-count=3 func TestHighAvailabilityGCE(t *testing.T) { runTestGCE(t, "ha-gce.example.com", "../../tests/integration/ha_gce", "v1alpha2", false, 3) } // TestComplex runs the test on a more complex configuration, intended to hit more of the edge cases func TestComplex(t *testing.T) { - runTestAWS(t, "complex.example.com", "../../tests/integration/complex", "v1alpha2", false, 1) + runTestAWS(t, "complex.example.com", "../../tests/integration/complex", "v1alpha2", false, 1, true) } // TestMinimalCloudformation runs the test on a minimum configuration, similar to kops create cluster minimal.example.com --zones us-west-1a @@ -78,56 +76,62 @@ func TestMinimalCloudformation(t *testing.T) { // TestMinimal_141 runs the test on a configuration from 1.4.1 release func TestMinimal_141(t *testing.T) { - runTestAWS(t, "minimal-141.example.com", "../../tests/integration/minimal-141", "v1alpha0", false, 1) + runTestAWS(t, "minimal-141.example.com", "../../tests/integration/minimal-141", "v1alpha0", false, 1, true) } // TestPrivateWeave runs the test on a configuration with private topology, weave networking func TestPrivateWeave(t *testing.T) { - runTestAWS(t, "privateweave.example.com", "../../tests/integration/privateweave", "v1alpha1", true, 1) - runTestAWS(t, "privateweave.example.com", "../../tests/integration/privateweave", "v1alpha2", true, 1) + runTestAWS(t, "privateweave.example.com", "../../tests/integration/privateweave", "v1alpha1", true, 1, true) + runTestAWS(t, "privateweave.example.com", "../../tests/integration/privateweave", "v1alpha2", true, 1, true) } // TestPrivateFlannel runs the test on a configuration with private topology, flannel networking func TestPrivateFlannel(t *testing.T) { - runTestAWS(t, "privateflannel.example.com", "../../tests/integration/privateflannel", "v1alpha1", true, 1) - runTestAWS(t, "privateflannel.example.com", "../../tests/integration/privateflannel", "v1alpha2", true, 1) + runTestAWS(t, "privateflannel.example.com", "../../tests/integration/privateflannel", "v1alpha1", true, 1, true) + runTestAWS(t, "privateflannel.example.com", "../../tests/integration/privateflannel", "v1alpha2", true, 1, true) } // TestPrivateCalico runs the test on a configuration with private topology, calico networking func TestPrivateCalico(t *testing.T) { - runTestAWS(t, "privatecalico.example.com", "../../tests/integration/privatecalico", "v1alpha1", true, 1) - runTestAWS(t, "privatecalico.example.com", "../../tests/integration/privatecalico", "v1alpha2", true, 1) + runTestAWS(t, "privatecalico.example.com", "../../tests/integration/privatecalico", "v1alpha1", true, 1, true) + runTestAWS(t, "privatecalico.example.com", "../../tests/integration/privatecalico", "v1alpha2", true, 1, true) } // TestPrivateCanal runs the test on a configuration with private topology, canal networking func TestPrivateCanal(t *testing.T) { - runTestAWS(t, "privatecanal.example.com", "../../tests/integration/privatecanal", "v1alpha1", true, 1) - runTestAWS(t, "privatecanal.example.com", "../../tests/integration/privatecanal", "v1alpha2", true, 1) + runTestAWS(t, "privatecanal.example.com", "../../tests/integration/privatecanal", "v1alpha1", true, 1, true) + runTestAWS(t, "privatecanal.example.com", "../../tests/integration/privatecanal", "v1alpha2", true, 1, true) } // TestPrivateKopeio runs the test on a configuration with private topology, kopeio networking func TestPrivateKopeio(t *testing.T) { - runTestAWS(t, "privatekopeio.example.com", "../../tests/integration/privatekopeio", "v1alpha2", true, 1) + runTestAWS(t, "privatekopeio.example.com", "../../tests/integration/privatekopeio", "v1alpha2", true, 1, true) } // TestPrivateDns1 runs the test on a configuration with private topology, private dns func TestPrivateDns1(t *testing.T) { - runTestAWS(t, "privatedns1.example.com", "../../tests/integration/privatedns1", "v1alpha2", true, 1) + runTestAWS(t, "privatedns1.example.com", "../../tests/integration/privatedns1", "v1alpha2", true, 1, true) } // TestPrivateDns2 runs the test on a configuration with private topology, private dns, extant vpc func TestPrivateDns2(t *testing.T) { - runTestAWS(t, "privatedns2.example.com", "../../tests/integration/privatedns2", "v1alpha2", true, 1) + runTestAWS(t, "privatedns2.example.com", "../../tests/integration/privatedns2", "v1alpha2", true, 1, true) } // TestSharedSubnet runs the test on a configuration with a shared subnet (and VPC) func TestSharedSubnet(t *testing.T) { - runTestAWS(t, "sharedsubnet.example.com", "../../tests/integration/shared_subnet", "v1alpha2", false, 1) + runTestAWS(t, "sharedsubnet.example.com", "../../tests/integration/shared_subnet", "v1alpha2", false, 1, true) } // TestSharedVPC runs the test on a configuration with a shared VPC func TestSharedVPC(t *testing.T) { - runTestAWS(t, "sharedvpc.example.com", "../../tests/integration/shared_vpc", "v1alpha2", false, 1) + runTestAWS(t, "sharedvpc.example.com", "../../tests/integration/shared_vpc", "v1alpha2", false, 1, true) +} + +// TestCreateClusterCustomAuthProfile runs kops create cluster custom_iam_role.example.com --zones us-test-1a +func TestCreateClusterCustomAuthProfile(t *testing.T) { + featureflag.ParseFlags("+CustomAuthProfileSupport") + runTestAWS(t, "custom-iam-role.example.com", "../../tests/integration/custom_iam_role", "v1alpha2", false, 1, false) } func runTest(t *testing.T, h *testutils.IntegrationTestHarness, clusterName string, srcDir string, version string, private bool, zones int, expectedFilenames []string) { @@ -236,20 +240,23 @@ func runTest(t *testing.T, h *testutils.IntegrationTestHarness, clusterName stri } } -func runTestAWS(t *testing.T, clusterName string, srcDir string, version string, private bool, zones int) { +func runTestAWS(t *testing.T, clusterName string, srcDir string, version string, private bool, zones int, expectPolicies bool) { h := testutils.NewIntegrationTestHarness(t) defer h.Close() h.SetupMockAWS() expectedFilenames := []string{ - "aws_iam_role_masters." + clusterName + "_policy", - "aws_iam_role_nodes." + clusterName + "_policy", - "aws_iam_role_policy_masters." + clusterName + "_policy", - "aws_iam_role_policy_nodes." + clusterName + "_policy", "aws_key_pair_kubernetes." + clusterName + "-c4a6ed9aa889b9e2c39cd663eb9c7157_public_key", "aws_launch_configuration_nodes." + clusterName + "_user_data", } + if expectPolicies { + expectedFilenames = append(expectedFilenames, + "aws_iam_role_masters."+clusterName+"_policy", + "aws_iam_role_nodes."+clusterName+"_policy", + "aws_iam_role_policy_masters."+clusterName+"_policy", + "aws_iam_role_policy_nodes."+clusterName+"_policy") + } for i := 0; i < zones; i++ { zone := "us-test-1" + string([]byte{byte('a') + byte(i)}) diff --git a/docs/cluster_spec.md b/docs/cluster_spec.md index cc1ea69c194ec..a29bfb9286fc2 100644 --- a/docs/cluster_spec.md +++ b/docs/cluster_spec.md @@ -19,6 +19,23 @@ spec: dns: {} ``` +### authProfile ALPHA SUPPORT + +This configuration allows a cluster to utilize existing IAM instance profiles. Currently this configuration only supports aws. +In order to use this feature you have to have to have the instance profile arn of a pre-existing role, and use the kops feature flag by setting +`export KOPS_FEATURE_FLAGS=+CustomAuthProfileSupport`. This feature is in ALPHA release only, and can cause very unusual behavior +with Kubernetes if use incorrectly. + +AuthRole example: + +```yaml +spec: + authPofile: + master: arn:aws:iam::123417490108:instance-profile/kops-custom-master-role + node: arn:aws:iam::123417490108:instance-profile/kops-custom-node-role +``` + +### api When configuring a LoadBalancer, you can also choose to have a public ELB or an internal (VPC only) ELB. The `type` field should be `Public` or `Internal`. diff --git a/docs/iam_roles.md b/docs/iam_roles.md index bb2a0c4842346..ce584dea5dd56 100644 --- a/docs/iam_roles.md +++ b/docs/iam_roles.md @@ -2,9 +2,9 @@ Two IAM roles are created for the cluster: one for the masters, and one for the nodes. -> Work is being done on scoping permissions to the minimum required to setup and maintain cluster. +> Work is being done on scoping permissions to the minimum required to setup and maintain cluster. > Please note that currently all Pods running on your cluster have access to instance IAM role. -> Consider using projects such as [kube2iam](https://github.com/jtblin/kube2iam) to prevent that. +> Consider using projects such as [kube2iam](https://github.com/jtblin/kube2iam) to prevent that. Master permissions: @@ -136,3 +136,30 @@ You can have an additional policy for each kops role (node, master, bastion). Fo } ] ``` + +## Reusing Existing Instance Profile + +Sometimes you may need to reuse existing IAM Instance Profiles. You can do this +through the `authProfile` cluster spec API field. This setting is highly advanced +and only enabled via CustomAuthProfileSupport`` feature flag. Setting the wrong role +permissions can impact various components inside of Kubernetes, and cause +unexpected issues. This feature is in place to support the initial documenting and testing the creation of custom roles. Again, use the existing kops functionality, or reach out +if you want to help! + +At this point, we do not have a full definition of the fine grain roles. Please refer +[to](https://github.com/kubernetes/kops/issues/1873) for more information. + +Please use this feature wisely! Enable the feature flag by: + +```console +$ export KOPS_FEATURE_FLAGS="+CustomAuthProfileSupport" +``` +Inside the cluster spec define one or two instance profiles specific to the master and +a node. + +```yaml +spec: + authPofile: + master: arn:aws:iam::123417490108:instance-profile/kops-custom-master-role + node: arn:aws:iam::123417490108:instance-profile/kops-custom-node-role +``` diff --git a/pkg/apis/kops/cluster.go b/pkg/apis/kops/cluster.go index e171e87850feb..75917f2ada81a 100644 --- a/pkg/apis/kops/cluster.go +++ b/pkg/apis/kops/cluster.go @@ -114,7 +114,13 @@ type ClusterSpec struct { // 'external' do not apply updates automatically - they are applied manually or by an external system // missing: default policy (currently OS security upgrades that do not require a reboot) UpdatePolicy *string `json:"updatePolicy,omitempty"` + + // AutoProfile an existing custom cloud security policy name for the instances. + // Only supported for AWS + AuthProfile *AuthProfile `json:"authProfile,omitempty"` + // Additional policies to add for roles + // Map is keyed by: master, node AdditionalPolicies *map[string]string `json:"additionalPolicies,omitempty"` // A collection of files assets for deployed cluster wide FileAssets []FileAssetSpec `json:"fileAssets,omitempty"` @@ -279,6 +285,27 @@ type ExternalDNSConfig struct { WatchNamespace string `json:"watchNamespace,omitempty"` } +// AuthProfile are the names of different instance profiles to use for IAM +// At this point only AWS is supported for this option. +// This is a very advanced option, which can really impact a kubernets cluster if not used properly, +// or open security holes as wel. We recommend using kops to construct the profile, or re-using a +// duplicate profile that kops uses. If users are not able to create auth profiles, a user +// with the correct auth can run `kops update` using the iam phase. +type AuthProfile struct { + + // Master is the name of the instance profile to use for the master + // Format expected is arn:aws:iam::123456789012:instance-profile/ExampleMasterRole + Master *string `json:"master,omitempty"` + + // Node is the name of the instance profile to use for the node + // Format expected is arn:aws:iam::123456789012:instance-profile/ExampleNodeRole + Node *string `json:"node,omitempty"` + + // Bastion is the name of the instance profile to use for the bastion + // Format expected is arn:aws:iam::123456789012:instance-profile/ExampleBastionRole + Bastion *string `json:"bastion,omitempty"` +} + // EtcdClusterSpec is the etcd cluster specification type EtcdClusterSpec struct { // Name is the name of the etcd cluster (main, events etc) diff --git a/pkg/apis/kops/v1alpha1/cluster.go b/pkg/apis/kops/v1alpha1/cluster.go index 30d3072075ea9..66cc14e62a6a4 100644 --- a/pkg/apis/kops/v1alpha1/cluster.go +++ b/pkg/apis/kops/v1alpha1/cluster.go @@ -109,6 +109,11 @@ type ClusterSpec struct { // 'external' do not apply updates automatically - they are applied manually or by an external system // missing: default policy (currently OS security upgrades that do not require a reboot) UpdatePolicy *string `json:"updatePolicy,omitempty"` + + // AutoProfile an existing custom cloud security policy name for the instances. + // Only supported for AWS + AuthProfile *AuthProfile `json:"authProfile,omitempty"` + // Additional policies to add for roles AdditionalPolicies *map[string]string `json:"additionalPolicies,omitempty"` // A collection of files assets for deployed cluster wide @@ -278,6 +283,27 @@ type ExternalDNSConfig struct { WatchNamespace string `json:"watchNamespace,omitempty"` } +// AuthProfile are the names of different instance profiles to use for IAM +// At this point only AWS is supported for this option. +// This is a very advanced option, which can really impact a kubernets cluster if not used properly, +// or open security holes as wel. We recommend using kops to construct the profile, or re-using a +// duplicate profile that kops uses. If users are not able to create auth profiles, a user +// with the correct auth can run `kops update` using the iam phase. +type AuthProfile struct { + + // Master is the name of the instance profile to use for the master + // Format expected is arn:aws:iam::123456789012:instance-profile/ExampleMasterRole + Master *string `json:"master,omitempty"` + + // Node is the name of the instance profile to use for the node + // Format expected is arn:aws:iam::123456789012:instance-profile/ExampleNodeRole + Node *string `json:"node,omitempty"` + + // Bastion is the name of the instance profile to use for the bastion + // Format expected is arn:aws:iam::123456789012:instance-profile/ExampleBastionRole + Bastion *string `json:"bastion,omitempty"` +} + // EtcdClusterSpec is the etcd cluster specification type EtcdClusterSpec struct { // Name is the name of the etcd cluster (main, events etc) diff --git a/pkg/apis/kops/v1alpha1/zz_generated.conversion.go b/pkg/apis/kops/v1alpha1/zz_generated.conversion.go index 5f43a090f92cb..b94e04237250e 100644 --- a/pkg/apis/kops/v1alpha1/zz_generated.conversion.go +++ b/pkg/apis/kops/v1alpha1/zz_generated.conversion.go @@ -41,6 +41,8 @@ func RegisterConversions(scheme *runtime.Scheme) error { Convert_kops_AlwaysAllowAuthorizationSpec_To_v1alpha1_AlwaysAllowAuthorizationSpec, Convert_v1alpha1_Assets_To_kops_Assets, Convert_kops_Assets_To_v1alpha1_Assets, + Convert_v1alpha1_AuthProfile_To_kops_AuthProfile, + Convert_kops_AuthProfile_To_v1alpha1_AuthProfile, Convert_v1alpha1_AuthenticationSpec_To_kops_AuthenticationSpec, Convert_kops_AuthenticationSpec_To_v1alpha1_AuthenticationSpec, Convert_v1alpha1_AuthorizationSpec_To_kops_AuthorizationSpec, @@ -55,6 +57,8 @@ func RegisterConversions(scheme *runtime.Scheme) error { Convert_kops_ClassicNetworkingSpec_To_v1alpha1_ClassicNetworkingSpec, Convert_v1alpha1_CloudConfiguration_To_kops_CloudConfiguration, Convert_kops_CloudConfiguration_To_v1alpha1_CloudConfiguration, + Convert_v1alpha1_CloudControllerManagerConfig_To_kops_CloudControllerManagerConfig, + Convert_kops_CloudControllerManagerConfig_To_v1alpha1_CloudControllerManagerConfig, Convert_v1alpha1_Cluster_To_kops_Cluster, Convert_kops_Cluster_To_v1alpha1_Cluster, Convert_v1alpha1_ClusterList_To_kops_ClusterList, @@ -109,8 +113,6 @@ func RegisterConversions(scheme *runtime.Scheme) error { Convert_kops_KubeAPIServerConfig_To_v1alpha1_KubeAPIServerConfig, Convert_v1alpha1_KubeControllerManagerConfig_To_kops_KubeControllerManagerConfig, Convert_kops_KubeControllerManagerConfig_To_v1alpha1_KubeControllerManagerConfig, - Convert_v1alpha1_CloudControllerManagerConfig_To_kops_CloudControllerManagerConfig, - Convert_kops_CloudControllerManagerConfig_To_v1alpha1_CloudControllerManagerConfig, Convert_v1alpha1_KubeDNSConfig_To_kops_KubeDNSConfig, Convert_kops_KubeDNSConfig_To_v1alpha1_KubeDNSConfig, Convert_v1alpha1_KubeProxyConfig_To_kops_KubeProxyConfig, @@ -238,6 +240,30 @@ func Convert_kops_Assets_To_v1alpha1_Assets(in *kops.Assets, out *Assets, s conv return autoConvert_kops_Assets_To_v1alpha1_Assets(in, out, s) } +func autoConvert_v1alpha1_AuthProfile_To_kops_AuthProfile(in *AuthProfile, out *kops.AuthProfile, s conversion.Scope) error { + out.Master = in.Master + out.Node = in.Node + out.Bastion = in.Bastion + return nil +} + +// Convert_v1alpha1_AuthProfile_To_kops_AuthProfile is an autogenerated conversion function. +func Convert_v1alpha1_AuthProfile_To_kops_AuthProfile(in *AuthProfile, out *kops.AuthProfile, s conversion.Scope) error { + return autoConvert_v1alpha1_AuthProfile_To_kops_AuthProfile(in, out, s) +} + +func autoConvert_kops_AuthProfile_To_v1alpha1_AuthProfile(in *kops.AuthProfile, out *AuthProfile, s conversion.Scope) error { + out.Master = in.Master + out.Node = in.Node + out.Bastion = in.Bastion + return nil +} + +// Convert_kops_AuthProfile_To_v1alpha1_AuthProfile is an autogenerated conversion function. +func Convert_kops_AuthProfile_To_v1alpha1_AuthProfile(in *kops.AuthProfile, out *AuthProfile, s conversion.Scope) error { + return autoConvert_kops_AuthProfile_To_v1alpha1_AuthProfile(in, out, s) +} + func autoConvert_v1alpha1_AuthenticationSpec_To_kops_AuthenticationSpec(in *AuthenticationSpec, out *kops.AuthenticationSpec, s conversion.Scope) error { if in.Kopeio != nil { in, out := &in.Kopeio, &out.Kopeio @@ -456,6 +482,60 @@ func Convert_kops_CloudConfiguration_To_v1alpha1_CloudConfiguration(in *kops.Clo return autoConvert_kops_CloudConfiguration_To_v1alpha1_CloudConfiguration(in, out, s) } +func autoConvert_v1alpha1_CloudControllerManagerConfig_To_kops_CloudControllerManagerConfig(in *CloudControllerManagerConfig, out *kops.CloudControllerManagerConfig, s conversion.Scope) error { + out.Master = in.Master + out.LogLevel = in.LogLevel + out.Image = in.Image + out.CloudProvider = in.CloudProvider + out.ClusterName = in.ClusterName + out.ClusterCIDR = in.ClusterCIDR + out.AllocateNodeCIDRs = in.AllocateNodeCIDRs + out.ConfigureCloudRoutes = in.ConfigureCloudRoutes + if in.LeaderElection != nil { + in, out := &in.LeaderElection, &out.LeaderElection + *out = new(kops.LeaderElectionConfiguration) + if err := Convert_v1alpha1_LeaderElectionConfiguration_To_kops_LeaderElectionConfiguration(*in, *out, s); err != nil { + return err + } + } else { + out.LeaderElection = nil + } + out.UseServiceAccountCredentials = in.UseServiceAccountCredentials + return nil +} + +// Convert_v1alpha1_CloudControllerManagerConfig_To_kops_CloudControllerManagerConfig is an autogenerated conversion function. +func Convert_v1alpha1_CloudControllerManagerConfig_To_kops_CloudControllerManagerConfig(in *CloudControllerManagerConfig, out *kops.CloudControllerManagerConfig, s conversion.Scope) error { + return autoConvert_v1alpha1_CloudControllerManagerConfig_To_kops_CloudControllerManagerConfig(in, out, s) +} + +func autoConvert_kops_CloudControllerManagerConfig_To_v1alpha1_CloudControllerManagerConfig(in *kops.CloudControllerManagerConfig, out *CloudControllerManagerConfig, s conversion.Scope) error { + out.Master = in.Master + out.LogLevel = in.LogLevel + out.Image = in.Image + out.CloudProvider = in.CloudProvider + out.ClusterName = in.ClusterName + out.ClusterCIDR = in.ClusterCIDR + out.AllocateNodeCIDRs = in.AllocateNodeCIDRs + out.ConfigureCloudRoutes = in.ConfigureCloudRoutes + if in.LeaderElection != nil { + in, out := &in.LeaderElection, &out.LeaderElection + *out = new(LeaderElectionConfiguration) + if err := Convert_kops_LeaderElectionConfiguration_To_v1alpha1_LeaderElectionConfiguration(*in, *out, s); err != nil { + return err + } + } else { + out.LeaderElection = nil + } + out.UseServiceAccountCredentials = in.UseServiceAccountCredentials + return nil +} + +// Convert_kops_CloudControllerManagerConfig_To_v1alpha1_CloudControllerManagerConfig is an autogenerated conversion function. +func Convert_kops_CloudControllerManagerConfig_To_v1alpha1_CloudControllerManagerConfig(in *kops.CloudControllerManagerConfig, out *CloudControllerManagerConfig, s conversion.Scope) error { + return autoConvert_kops_CloudControllerManagerConfig_To_v1alpha1_CloudControllerManagerConfig(in, out, s) +} + func autoConvert_v1alpha1_Cluster_To_kops_Cluster(in *Cluster, out *kops.Cluster, s conversion.Scope) error { out.ObjectMeta = in.ObjectMeta if err := Convert_v1alpha1_ClusterSpec_To_kops_ClusterSpec(&in.Spec, &out.Spec, s); err != nil { @@ -555,6 +635,15 @@ func autoConvert_v1alpha1_ClusterSpec_To_kops_ClusterSpec(in *ClusterSpec, out * // WARNING: in.AdminAccess requires manual conversion: does not exist in peer-type out.IsolateMasters = in.IsolateMasters out.UpdatePolicy = in.UpdatePolicy + if in.AuthProfile != nil { + in, out := &in.AuthProfile, &out.AuthProfile + *out = new(kops.AuthProfile) + if err := Convert_v1alpha1_AuthProfile_To_kops_AuthProfile(*in, *out, s); err != nil { + return err + } + } else { + out.AuthProfile = nil + } out.AdditionalPolicies = in.AdditionalPolicies if in.FileAssets != nil { in, out := &in.FileAssets, &out.FileAssets @@ -800,6 +889,15 @@ func autoConvert_kops_ClusterSpec_To_v1alpha1_ClusterSpec(in *kops.ClusterSpec, // WARNING: in.KubernetesAPIAccess requires manual conversion: does not exist in peer-type out.IsolateMasters = in.IsolateMasters out.UpdatePolicy = in.UpdatePolicy + if in.AuthProfile != nil { + in, out := &in.AuthProfile, &out.AuthProfile + *out = new(AuthProfile) + if err := Convert_kops_AuthProfile_To_v1alpha1_AuthProfile(*in, *out, s); err != nil { + return err + } + } else { + out.AuthProfile = nil + } out.AdditionalPolicies = in.AdditionalPolicies if in.FileAssets != nil { in, out := &in.FileAssets, &out.FileAssets @@ -1886,59 +1984,6 @@ func autoConvert_v1alpha1_KubeDNSConfig_To_kops_KubeDNSConfig(in *KubeDNSConfig, return nil } -func autoConvert_v1alpha1_CloudControllerManagerConfig_To_kops_CloudControllerManagerConfig(in *CloudControllerManagerConfig, out *kops.CloudControllerManagerConfig, s conversion.Scope) error { - out.Master = in.Master - out.LogLevel = in.LogLevel - out.Image = in.Image - out.CloudProvider = in.CloudProvider - out.ClusterName = in.ClusterName - out.ClusterCIDR = in.ClusterCIDR - out.AllocateNodeCIDRs = in.AllocateNodeCIDRs - out.ConfigureCloudRoutes = in.ConfigureCloudRoutes - if in.LeaderElection != nil { - in, out := &in.LeaderElection, &out.LeaderElection - *out = new(kops.LeaderElectionConfiguration) - if err := Convert_v1alpha1_LeaderElectionConfiguration_To_kops_LeaderElectionConfiguration(*in, *out, s); err != nil { - return err - } - } else { - out.LeaderElection = nil - } - out.UseServiceAccountCredentials = in.UseServiceAccountCredentials - return nil -} - -// Convert_v1alpha1_CloudControllerManagerConfig_To_kops_CloudControllerManagerConfig is an autogenerated conversion function. -func Convert_v1alpha1_CloudControllerManagerConfig_To_kops_CloudControllerManagerConfig(in *CloudControllerManagerConfig, out *kops.CloudControllerManagerConfig, s conversion.Scope) error { - return autoConvert_v1alpha1_CloudControllerManagerConfig_To_kops_CloudControllerManagerConfig(in, out, s) -} - -func autoConvert_kops_CloudControllerManagerConfig_To_v1alpha1_CloudControllerManagerConfig(in *kops.CloudControllerManagerConfig, out *CloudControllerManagerConfig, s conversion.Scope) error { - out.Master = in.Master - out.LogLevel = in.LogLevel - out.Image = in.Image - out.CloudProvider = in.CloudProvider - out.ClusterName = in.ClusterName - out.ClusterCIDR = in.ClusterCIDR - out.AllocateNodeCIDRs = in.AllocateNodeCIDRs - out.ConfigureCloudRoutes = in.ConfigureCloudRoutes - if in.LeaderElection != nil { - in, out := &in.LeaderElection, &out.LeaderElection - *out = new(LeaderElectionConfiguration) - if err := Convert_kops_LeaderElectionConfiguration_To_v1alpha1_LeaderElectionConfiguration(*in, *out, s); err != nil { - return err - } - } else { - out.LeaderElection = nil - } - return nil -} - -// Convert_kops_CloudControllerManagerConfig_To_v1alpha1_CloudControllerManagerConfig is an autogenerated conversion function. -func Convert_kops_CloudControllerManagerConfig_To_v1alpha1_CloudControllerManagerConfig(in *kops.CloudControllerManagerConfig, out *CloudControllerManagerConfig, s conversion.Scope) error { - return autoConvert_kops_CloudControllerManagerConfig_To_v1alpha1_CloudControllerManagerConfig(in, out, s) -} - // Convert_v1alpha1_KubeDNSConfig_To_kops_KubeDNSConfig is an autogenerated conversion function. func Convert_v1alpha1_KubeDNSConfig_To_kops_KubeDNSConfig(in *KubeDNSConfig, out *kops.KubeDNSConfig, s conversion.Scope) error { return autoConvert_v1alpha1_KubeDNSConfig_To_kops_KubeDNSConfig(in, out, s) diff --git a/pkg/apis/kops/v1alpha2/cluster.go b/pkg/apis/kops/v1alpha2/cluster.go index f21671557f172..cf0b9248313df 100644 --- a/pkg/apis/kops/v1alpha2/cluster.go +++ b/pkg/apis/kops/v1alpha2/cluster.go @@ -114,6 +114,11 @@ type ClusterSpec struct { // 'external' do not apply updates automatically - they are applied manually or by an external system // missing: default policy (currently OS security upgrades that do not require a reboot) UpdatePolicy *string `json:"updatePolicy,omitempty"` + + // AutoProfile an existing custom cloud security policy name for the instances. + // Only supported for AWS + AuthProfile *AuthProfile `json:"authProfile,omitempty"` + // Additional policies to add for roles AdditionalPolicies *map[string]string `json:"additionalPolicies,omitempty"` // A collection of files assets for deployed cluster wide @@ -345,3 +350,24 @@ type HTTPProxy struct { // User string `json:"user,omitempty"` // Password string `json:"password,omitempty"` } + +// AuthProfile are the names of different instance profiles to use for IAM +// At this point only AWS is supported for this option. +// This is a very advanced option, which can really impact a kubernets cluster if not used properly, +// or open security holes as wel. We recommend using kops to construct the profile, or re-using a +// duplicate profile that kops uses. If users are not able to create auth profiles, a user +// with the correct auth can run `kops update` using the iam phase. +type AuthProfile struct { + + // Master is the name of the instance profile to use for the master + // Format expected is arn:aws:iam::123456789012:instance-profile/ExampleMasterRole + Master *string `json:"master,omitempty"` + + // Node is the name of the instance profile to use for the node + // Format expected is arn:aws:iam::123456789012:instance-profile/ExampleNodeRole + Node *string `json:"node,omitempty"` + + // Bastion is the name of the instance profile to use for the bastion + // Format expected is arn:aws:iam::123456789012:instance-profile/ExampleBastionRole + Bastion *string `json:"bastion,omitempty"` +} diff --git a/pkg/apis/kops/v1alpha2/zz_generated.conversion.go b/pkg/apis/kops/v1alpha2/zz_generated.conversion.go index 8bf7ff5799403..0dcba93eabe9e 100644 --- a/pkg/apis/kops/v1alpha2/zz_generated.conversion.go +++ b/pkg/apis/kops/v1alpha2/zz_generated.conversion.go @@ -41,6 +41,8 @@ func RegisterConversions(scheme *runtime.Scheme) error { Convert_kops_AlwaysAllowAuthorizationSpec_To_v1alpha2_AlwaysAllowAuthorizationSpec, Convert_v1alpha2_Assets_To_kops_Assets, Convert_kops_Assets_To_v1alpha2_Assets, + Convert_v1alpha2_AuthProfile_To_kops_AuthProfile, + Convert_kops_AuthProfile_To_v1alpha2_AuthProfile, Convert_v1alpha2_AuthenticationSpec_To_kops_AuthenticationSpec, Convert_kops_AuthenticationSpec_To_v1alpha2_AuthenticationSpec, Convert_v1alpha2_AuthorizationSpec_To_kops_AuthorizationSpec, @@ -57,6 +59,8 @@ func RegisterConversions(scheme *runtime.Scheme) error { Convert_kops_ClassicNetworkingSpec_To_v1alpha2_ClassicNetworkingSpec, Convert_v1alpha2_CloudConfiguration_To_kops_CloudConfiguration, Convert_kops_CloudConfiguration_To_v1alpha2_CloudConfiguration, + Convert_v1alpha2_CloudControllerManagerConfig_To_kops_CloudControllerManagerConfig, + Convert_kops_CloudControllerManagerConfig_To_v1alpha2_CloudControllerManagerConfig, Convert_v1alpha2_Cluster_To_kops_Cluster, Convert_kops_Cluster_To_v1alpha2_Cluster, Convert_v1alpha2_ClusterList_To_kops_ClusterList, @@ -121,8 +125,6 @@ func RegisterConversions(scheme *runtime.Scheme) error { Convert_kops_KubeAPIServerConfig_To_v1alpha2_KubeAPIServerConfig, Convert_v1alpha2_KubeControllerManagerConfig_To_kops_KubeControllerManagerConfig, Convert_kops_KubeControllerManagerConfig_To_v1alpha2_KubeControllerManagerConfig, - Convert_v1alpha2_CloudControllerManagerConfig_To_kops_CloudControllerManagerConfig, - Convert_kops_CloudControllerManagerConfig_To_v1alpha2_CloudControllerManagerConfig, Convert_v1alpha2_KubeDNSConfig_To_kops_KubeDNSConfig, Convert_kops_KubeDNSConfig_To_v1alpha2_KubeDNSConfig, Convert_v1alpha2_KubeProxyConfig_To_kops_KubeProxyConfig, @@ -252,6 +254,30 @@ func Convert_kops_Assets_To_v1alpha2_Assets(in *kops.Assets, out *Assets, s conv return autoConvert_kops_Assets_To_v1alpha2_Assets(in, out, s) } +func autoConvert_v1alpha2_AuthProfile_To_kops_AuthProfile(in *AuthProfile, out *kops.AuthProfile, s conversion.Scope) error { + out.Master = in.Master + out.Node = in.Node + out.Bastion = in.Bastion + return nil +} + +// Convert_v1alpha2_AuthProfile_To_kops_AuthProfile is an autogenerated conversion function. +func Convert_v1alpha2_AuthProfile_To_kops_AuthProfile(in *AuthProfile, out *kops.AuthProfile, s conversion.Scope) error { + return autoConvert_v1alpha2_AuthProfile_To_kops_AuthProfile(in, out, s) +} + +func autoConvert_kops_AuthProfile_To_v1alpha2_AuthProfile(in *kops.AuthProfile, out *AuthProfile, s conversion.Scope) error { + out.Master = in.Master + out.Node = in.Node + out.Bastion = in.Bastion + return nil +} + +// Convert_kops_AuthProfile_To_v1alpha2_AuthProfile is an autogenerated conversion function. +func Convert_kops_AuthProfile_To_v1alpha2_AuthProfile(in *kops.AuthProfile, out *AuthProfile, s conversion.Scope) error { + return autoConvert_kops_AuthProfile_To_v1alpha2_AuthProfile(in, out, s) +} + func autoConvert_v1alpha2_AuthenticationSpec_To_kops_AuthenticationSpec(in *AuthenticationSpec, out *kops.AuthenticationSpec, s conversion.Scope) error { if in.Kopeio != nil { in, out := &in.Kopeio, &out.Kopeio @@ -492,6 +518,60 @@ func Convert_kops_CloudConfiguration_To_v1alpha2_CloudConfiguration(in *kops.Clo return autoConvert_kops_CloudConfiguration_To_v1alpha2_CloudConfiguration(in, out, s) } +func autoConvert_v1alpha2_CloudControllerManagerConfig_To_kops_CloudControllerManagerConfig(in *CloudControllerManagerConfig, out *kops.CloudControllerManagerConfig, s conversion.Scope) error { + out.Master = in.Master + out.LogLevel = in.LogLevel + out.Image = in.Image + out.CloudProvider = in.CloudProvider + out.ClusterName = in.ClusterName + out.ClusterCIDR = in.ClusterCIDR + out.AllocateNodeCIDRs = in.AllocateNodeCIDRs + out.ConfigureCloudRoutes = in.ConfigureCloudRoutes + if in.LeaderElection != nil { + in, out := &in.LeaderElection, &out.LeaderElection + *out = new(kops.LeaderElectionConfiguration) + if err := Convert_v1alpha2_LeaderElectionConfiguration_To_kops_LeaderElectionConfiguration(*in, *out, s); err != nil { + return err + } + } else { + out.LeaderElection = nil + } + out.UseServiceAccountCredentials = in.UseServiceAccountCredentials + return nil +} + +// Convert_v1alpha2_CloudControllerManagerConfig_To_kops_CloudControllerManagerConfig is an autogenerated conversion function. +func Convert_v1alpha2_CloudControllerManagerConfig_To_kops_CloudControllerManagerConfig(in *CloudControllerManagerConfig, out *kops.CloudControllerManagerConfig, s conversion.Scope) error { + return autoConvert_v1alpha2_CloudControllerManagerConfig_To_kops_CloudControllerManagerConfig(in, out, s) +} + +func autoConvert_kops_CloudControllerManagerConfig_To_v1alpha2_CloudControllerManagerConfig(in *kops.CloudControllerManagerConfig, out *CloudControllerManagerConfig, s conversion.Scope) error { + out.Master = in.Master + out.LogLevel = in.LogLevel + out.Image = in.Image + out.CloudProvider = in.CloudProvider + out.ClusterName = in.ClusterName + out.ClusterCIDR = in.ClusterCIDR + out.AllocateNodeCIDRs = in.AllocateNodeCIDRs + out.ConfigureCloudRoutes = in.ConfigureCloudRoutes + if in.LeaderElection != nil { + in, out := &in.LeaderElection, &out.LeaderElection + *out = new(LeaderElectionConfiguration) + if err := Convert_kops_LeaderElectionConfiguration_To_v1alpha2_LeaderElectionConfiguration(*in, *out, s); err != nil { + return err + } + } else { + out.LeaderElection = nil + } + out.UseServiceAccountCredentials = in.UseServiceAccountCredentials + return nil +} + +// Convert_kops_CloudControllerManagerConfig_To_v1alpha2_CloudControllerManagerConfig is an autogenerated conversion function. +func Convert_kops_CloudControllerManagerConfig_To_v1alpha2_CloudControllerManagerConfig(in *kops.CloudControllerManagerConfig, out *CloudControllerManagerConfig, s conversion.Scope) error { + return autoConvert_kops_CloudControllerManagerConfig_To_v1alpha2_CloudControllerManagerConfig(in, out, s) +} + func autoConvert_v1alpha2_Cluster_To_kops_Cluster(in *Cluster, out *kops.Cluster, s conversion.Scope) error { out.ObjectMeta = in.ObjectMeta if err := Convert_v1alpha2_ClusterSpec_To_kops_ClusterSpec(&in.Spec, &out.Spec, s); err != nil { @@ -612,6 +692,15 @@ func autoConvert_v1alpha2_ClusterSpec_To_kops_ClusterSpec(in *ClusterSpec, out * out.KubernetesAPIAccess = in.KubernetesAPIAccess out.IsolateMasters = in.IsolateMasters out.UpdatePolicy = in.UpdatePolicy + if in.AuthProfile != nil { + in, out := &in.AuthProfile, &out.AuthProfile + *out = new(kops.AuthProfile) + if err := Convert_v1alpha2_AuthProfile_To_kops_AuthProfile(*in, *out, s); err != nil { + return err + } + } else { + out.AuthProfile = nil + } out.AdditionalPolicies = in.AdditionalPolicies if in.FileAssets != nil { in, out := &in.FileAssets, &out.FileAssets @@ -862,6 +951,15 @@ func autoConvert_kops_ClusterSpec_To_v1alpha2_ClusterSpec(in *kops.ClusterSpec, out.KubernetesAPIAccess = in.KubernetesAPIAccess out.IsolateMasters = in.IsolateMasters out.UpdatePolicy = in.UpdatePolicy + if in.AuthProfile != nil { + in, out := &in.AuthProfile, &out.AuthProfile + *out = new(AuthProfile) + if err := Convert_kops_AuthProfile_To_v1alpha2_AuthProfile(*in, *out, s); err != nil { + return err + } + } else { + out.AuthProfile = nil + } out.AdditionalPolicies = in.AdditionalPolicies if in.FileAssets != nil { in, out := &in.FileAssets, &out.FileAssets @@ -2140,59 +2238,6 @@ func Convert_kops_KubeControllerManagerConfig_To_v1alpha2_KubeControllerManagerC return autoConvert_kops_KubeControllerManagerConfig_To_v1alpha2_KubeControllerManagerConfig(in, out, s) } -func autoConvert_v1alpha2_CloudControllerManagerConfig_To_kops_CloudControllerManagerConfig(in *CloudControllerManagerConfig, out *kops.CloudControllerManagerConfig, s conversion.Scope) error { - out.Master = in.Master - out.LogLevel = in.LogLevel - out.Image = in.Image - out.CloudProvider = in.CloudProvider - out.ClusterName = in.ClusterName - out.ClusterCIDR = in.ClusterCIDR - out.AllocateNodeCIDRs = in.AllocateNodeCIDRs - out.ConfigureCloudRoutes = in.ConfigureCloudRoutes - if in.LeaderElection != nil { - in, out := &in.LeaderElection, &out.LeaderElection - *out = new(kops.LeaderElectionConfiguration) - if err := Convert_v1alpha2_LeaderElectionConfiguration_To_kops_LeaderElectionConfiguration(*in, *out, s); err != nil { - return err - } - } else { - out.LeaderElection = nil - } - out.UseServiceAccountCredentials = in.UseServiceAccountCredentials - return nil -} - -// Convert_v1alpha2_CloudControllerManagerConfig_To_kops_CloudControllerManagerConfig is an autogenerated conversion function. -func Convert_v1alpha2_CloudControllerManagerConfig_To_kops_CloudControllerManagerConfig(in *CloudControllerManagerConfig, out *kops.CloudControllerManagerConfig, s conversion.Scope) error { - return autoConvert_v1alpha2_CloudControllerManagerConfig_To_kops_CloudControllerManagerConfig(in, out, s) -} - -func autoConvert_kops_CloudControllerManagerConfig_To_v1alpha2_CloudControllerManagerConfig(in *kops.CloudControllerManagerConfig, out *CloudControllerManagerConfig, s conversion.Scope) error { - out.Master = in.Master - out.LogLevel = in.LogLevel - out.Image = in.Image - out.CloudProvider = in.CloudProvider - out.ClusterName = in.ClusterName - out.ClusterCIDR = in.ClusterCIDR - out.AllocateNodeCIDRs = in.AllocateNodeCIDRs - out.ConfigureCloudRoutes = in.ConfigureCloudRoutes - if in.LeaderElection != nil { - in, out := &in.LeaderElection, &out.LeaderElection - *out = new(LeaderElectionConfiguration) - if err := Convert_kops_LeaderElectionConfiguration_To_v1alpha2_LeaderElectionConfiguration(*in, *out, s); err != nil { - return err - } - } else { - out.LeaderElection = nil - } - return nil -} - -// Convert_kops_CloudControllerManagerConfig_To_v1alpha2_CloudControllerManagerConfig is an autogenerated conversion function. -func Convert_kops_CloudControllerManagerConfig_To_v1alpha2_CloudControllerManagerConfig(in *kops.CloudControllerManagerConfig, out *CloudControllerManagerConfig, s conversion.Scope) error { - return autoConvert_kops_CloudControllerManagerConfig_To_v1alpha2_CloudControllerManagerConfig(in, out, s) -} - func autoConvert_v1alpha2_KubeDNSConfig_To_kops_KubeDNSConfig(in *KubeDNSConfig, out *kops.KubeDNSConfig, s conversion.Scope) error { out.Image = in.Image out.Replicas = in.Replicas diff --git a/pkg/apis/kops/validation/validation.go b/pkg/apis/kops/validation/validation.go index d81dac1a1b024..d4a512e31dbd7 100644 --- a/pkg/apis/kops/validation/validation.go +++ b/pkg/apis/kops/validation/validation.go @@ -19,6 +19,7 @@ package validation import ( "fmt" "net" + "regexp" "strings" "k8s.io/apimachinery/pkg/api/validation" @@ -89,6 +90,10 @@ func validateClusterSpec(spec *kops.ClusterSpec, fieldPath *field.Path) field.Er allErrs = append(allErrs, validateNetworking(spec.Networking, fieldPath.Child("networking"))...) } + if spec.AuthProfile != nil { + allErrs = append(allErrs, validateAuthProfile(spec.AuthProfile, fieldPath.Child("authProfile"))...) + } + return allErrs } @@ -244,3 +249,28 @@ func validateNetworkingFlannel(v *kops.FlannelNetworkingSpec, fldPath *field.Pat return allErrs } + +// format is arn:aws:iam::123456789012:instance-profile/S3Access +var validARN = regexp.MustCompile(`^arn:aws:iam::\d+:instance-profile\/\S+$`) + +// validateAuthProfile checks the String values for the AuthProfile +func validateAuthProfile(v *kops.AuthProfile, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + + if v.Node != nil { + arn := *v.Node + if !validARN.MatchString(arn) { + allErrs = append(allErrs, field.Invalid(fldPath.Child("Node"), arn, + "Node AuthProfile must be a valid aws arn such as arn:aws:iam::123456789012:instance-profile/KopsNodeExampleRole")) + } + } + if v.Master != nil { + arn := *v.Master + if !validARN.MatchString(arn) { + allErrs = append(allErrs, field.Invalid(fldPath.Child("Master"), arn, + "Node AuthProfile must be a valid aws arn such as arn:aws:iam::123456789012:instance-profile/KopsMasterExampleRole")) + } + } + + return allErrs +} diff --git a/pkg/apis/kops/validation/validation_test.go b/pkg/apis/kops/validation/validation_test.go index bd759552f6d7a..7d3827e2d4ada 100644 --- a/pkg/apis/kops/validation/validation_test.go +++ b/pkg/apis/kops/validation/validation_test.go @@ -23,6 +23,7 @@ import ( "k8s.io/apimachinery/pkg/util/validation" "k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/kops/pkg/apis/kops" + "k8s.io/kops/upup/pkg/fi" ) func Test_Validate_DNS(t *testing.T) { @@ -81,10 +82,69 @@ func TestValidateCIDR(t *testing.T) { } } +func s(v string) *string { + return fi.String(v) +} + +func TestValidateAuth(t *testing.T) { + grid := []struct { + Input *kops.AuthProfile + ExpectedErrors []string + ExpectedDetail string + }{ + { + Input: &kops.AuthProfile{ + Master: s("arn:aws:iam::123456789012:instance-profile/S3Access"), + }, + }, + { + Input: &kops.AuthProfile{ + Master: s("arn:aws:iam::123456789012:instance-profile/has/path/S3Access"), + }, + }, + { + Input: &kops.AuthProfile{ + Master: s("42"), + }, + ExpectedErrors: []string{"Invalid value::AuthProfile.Master"}, + ExpectedDetail: "Node AuthProfile must be a valid aws arn such as arn:aws:iam::123456789012:instance-profile/KopsMasterExampleRole", + }, + { + Input: &kops.AuthProfile{ + Node: s("arn:aws:iam::123456789012:group/division_abc/subdivision_xyz/product_A/Developers"), + }, + ExpectedErrors: []string{"Invalid value::AuthProfile.Node"}, + ExpectedDetail: "Node AuthProfile must be a valid aws arn such as arn:aws:iam::123456789012:instance-profile/KopsNodeExampleRole", + }, + } + + for _, g := range grid { + errs := validateAuthProfile(g.Input, field.NewPath("AuthProfile")) + + testErrors(t, g.Input, errs, g.ExpectedErrors) + + if g.ExpectedDetail != "" { + found := false + for _, err := range errs { + if err.Detail == g.ExpectedDetail { + found = true + } + } + if !found { + for _, err := range errs { + t.Logf("found detail: %q", err.Detail) + } + + t.Errorf("did not find expected error %q", g.ExpectedDetail) + } + } + } +} + func testErrors(t *testing.T, context interface{}, actual field.ErrorList, expectedErrors []string) { if len(expectedErrors) == 0 { if len(actual) != 0 { - t.Errorf("unexpected errors from %q: %v", context, actual) + t.Errorf("unexpected errors from %v: %+v", context, actual) } } else { errStrings := sets.NewString() diff --git a/pkg/featureflag/featureflag.go b/pkg/featureflag/featureflag.go index 4c2d78b1ba9e8..2aca80e475074 100644 --- a/pkg/featureflag/featureflag.go +++ b/pkg/featureflag/featureflag.go @@ -58,6 +58,9 @@ var EnableExternalCloudController = New("EnableExternalCloudController", Bool(fa // SpecOverrideFlag allows setting spec values on create var SpecOverrideFlag = New("SpecOverrideFlag", Bool(false)) +// CustomAuthProfileSupport if set will allow for the reuse of an existing security profile +var CustomAuthProfileSupport = New("CustomAuthProfileSupport", Bool(false)) + var flags = make(map[string]*FeatureFlag) var flagsMutex sync.Mutex diff --git a/pkg/model/awsmodel/autoscalinggroup.go b/pkg/model/awsmodel/autoscalinggroup.go index e1521d773d2bd..9b30ab6af0607 100644 --- a/pkg/model/awsmodel/autoscalinggroup.go +++ b/pkg/model/awsmodel/autoscalinggroup.go @@ -66,6 +66,11 @@ func (b *AutoscalingGroupModelBuilder) Build(c *fi.ModelBuilderContext) error { volumeType = DefaultVolumeType } + link, err := b.LinkToIAMInstanceProfile(ig) + if err != nil { + return fmt.Errorf("unable to find iam profile link for instance group %q: %v", ig.ObjectMeta.Name, err) + } + t := &awstasks.LaunchConfiguration{ Name: s(name), Lifecycle: b.Lifecycle, @@ -73,7 +78,7 @@ func (b *AutoscalingGroupModelBuilder) Build(c *fi.ModelBuilderContext) error { SecurityGroups: []*awstasks.SecurityGroup{ b.LinkToSecurityGroup(ig.Spec.Role), }, - IAMInstanceProfile: b.LinkToIAMInstanceProfile(ig), + IAMInstanceProfile: link, ImageID: s(ig.Spec.Image), InstanceType: s(ig.Spec.MachineType), diff --git a/pkg/model/iam.go b/pkg/model/iam.go index b4e64f04145c5..97594fd4e187b 100644 --- a/pkg/model/iam.go +++ b/pkg/model/iam.go @@ -24,7 +24,10 @@ import ( "text/template" "github.com/golang/glog" + + "k8s.io/apimachinery/pkg/util/sets" "k8s.io/kops/pkg/apis/kops" + "k8s.io/kops/pkg/featureflag" "k8s.io/kops/pkg/model/iam" "k8s.io/kops/upup/pkg/fi" "k8s.io/kops/upup/pkg/fi/cloudup/awstasks" @@ -51,23 +54,83 @@ const RolePolicyTemplate = `{ }` func (b *IAMModelBuilder) Build(c *fi.ModelBuilderContext) error { - // Collect the roles in use + hasAuthProfile := false + if b.Cluster.Spec.AuthProfile != nil { + hasAuthProfile = true + if !featureflag.CustomAuthProfileSupport.Enabled() { + glog.Warningf("found auth profile, but the feature flag is not enabled") + } + } + var roles []kops.InstanceGroupRole + hasRole := sets.NewString() + + // collect all roles in use for _, ig := range b.InstanceGroups { - found := false - for _, r := range roles { - if r == ig.Spec.Role { - found = true + // Test if we have a shared iam instance profile + // We are hiding this behind a feature flag, because this is can + // mess up clusters big time. + + // If we've specified a custom instance profile for this cluster role, + // do not create a task for a shared instance profile + // TODO Validate instance profile role against role that kops generates + // TODO We also need to validate that we do not have additional policies as well + if hasAuthProfile && featureflag.CustomAuthProfileSupport.Enabled() { + switch ig.Spec.Role { + case kops.InstanceGroupRoleBastion: + if b.Cluster.Spec.AuthProfile.Bastion != nil { + // this may not be the name, but we are setting it for the task + // we may want to set the id in launchconfiguration + if err := b.buildShareIAMInstanceProfile(b.Cluster.Spec.AuthProfile.Bastion, c); err != nil { + return fmt.Errorf("unable to get name for bastion role: %v", err) + } + // we set this so the role is not built + continue + } + case kops.InstanceGroupRoleNode: + if b.Cluster.Spec.AuthProfile.Node != nil { + // this may not be the name, but we are setting it for the task + // TODO we may want to set the id in launchconfiguration + if err := b.buildShareIAMInstanceProfile(b.Cluster.Spec.AuthProfile.Node, c); err != nil { + return fmt.Errorf("unable to get name for node role: %v", err) + } + // we set this so the role is not built + continue + } + case kops.InstanceGroupRoleMaster: + if b.Cluster.Spec.AuthProfile.Master != nil { + // this may not be the name, but we are setting it for the task + // TODO we may want to set the id in launchconfiguration + if err := b.buildShareIAMInstanceProfile(b.Cluster.Spec.AuthProfile.Master, c); err != nil { + return fmt.Errorf("unable to get name for master role: %v", err) + } + // we set this so the role is not built + continue + } + default: + return fmt.Errorf("unrecognised instance group type: %s", ig.Spec.Role) } } - if !found { + + if !hasRole.Has(string(ig.Spec.Role)) { roles = append(roles, ig.Spec.Role) } + + hasRole.Insert(string(ig.Spec.Role)) + + // we have found as many role as we have + if hasRole.Len() == len(kops.AllInstanceGroupRoles) { + break + } + } // Generate IAM objects etc for each role for _, role := range roles { - name := b.IAMName(role) + name, err := b.IAMName(role) + if err != nil { + return err + } var iamRole *awstasks.IAMRole { @@ -182,6 +245,25 @@ func (b *IAMModelBuilder) Build(c *fi.ModelBuilderContext) error { return nil } +// buildShareIAMInstanceProfile adds a shared IAMInstanceProfile task to the ModelBuilderContext +func (b *IAMModelBuilder) buildShareIAMInstanceProfile(id *string, c *fi.ModelBuilderContext) error { + name, err := findCustomAuthNameFromArn(id) + if err != nil { + return fmt.Errorf("unable to parse instance profile name from arn %q: %v", name, err) + } + var iamInstanceProfile *awstasks.IAMInstanceProfile + { + iamInstanceProfile = &awstasks.IAMInstanceProfile{ + Name: fi.String(name), + Lifecycle: b.Lifecycle, + ID: id, + Shared: fi.Bool(true), + } + c.AddTask(iamInstanceProfile) + } + return nil +} + // buildAWSIAMRolePolicy produces the AWS IAM role policy for the given role func (b *IAMModelBuilder) buildAWSIAMRolePolicy() (fi.Resource, error) { functions := template.FuncMap{ diff --git a/pkg/model/names.go b/pkg/model/names.go index 10ed1c11ffa5e..238bcfdae702e 100644 --- a/pkg/model/names.go +++ b/pkg/model/names.go @@ -18,8 +18,13 @@ package model import ( "fmt" + + "regexp" + "github.com/golang/glog" "k8s.io/kops/pkg/apis/kops" + "k8s.io/kops/pkg/featureflag" + "k8s.io/kops/upup/pkg/fi" "k8s.io/kops/upup/pkg/fi/cloudup/awstasks" ) @@ -96,24 +101,57 @@ func (b *KopsModelContext) NameForDNSZone() string { return name } -func (b *KopsModelContext) IAMName(role kops.InstanceGroupRole) string { +func (b *KopsModelContext) IAMName(role kops.InstanceGroupRole) (string, error) { + hasAuthProfile := false + if b.Cluster.Spec.AuthProfile != nil { + hasAuthProfile = true + } + switch role { case kops.InstanceGroupRoleMaster: - return "masters." + b.ClusterName() + if hasAuthProfile && featureflag.CustomAuthProfileSupport.Enabled() { + return findCustomAuthNameFromArn(b.Cluster.Spec.AuthProfile.Master) + } + return "masters." + b.ClusterName(), nil case kops.InstanceGroupRoleBastion: - return "bastions." + b.ClusterName() + if hasAuthProfile && featureflag.CustomAuthProfileSupport.Enabled() { + return findCustomAuthNameFromArn(b.Cluster.Spec.AuthProfile.Bastion) + } + return "bastions." + b.ClusterName(), nil case kops.InstanceGroupRoleNode: - return "nodes." + b.ClusterName() + if hasAuthProfile && featureflag.CustomAuthProfileSupport.Enabled() { + return findCustomAuthNameFromArn(b.Cluster.Spec.AuthProfile.Node) + } + return "nodes." + b.ClusterName(), nil default: - glog.Fatalf("unknown InstanceGroup Role: %q", role) - return "" + return "", fmt.Errorf("unknown InstanceGroup Role: %q", role) } } -func (b *KopsModelContext) LinkToIAMInstanceProfile(ig *kops.InstanceGroup) *awstasks.IAMInstanceProfile { - name := b.IAMName(ig.Spec.Role) - return &awstasks.IAMInstanceProfile{Name: &name} +var RoleNamRegExp = regexp.MustCompile(`([^/]+$)`) + +// findCustomAuthNameFromArn parses the name of a instance profile from the arn +func findCustomAuthNameFromArn(arn *string) (string, error) { + a := fi.StringValue(arn) + if a == "" { + return "", fmt.Errorf("unable to parse role arn as it is not set") + } + rs := RoleNamRegExp.FindStringSubmatch(a) + if len(rs) >= 2 { + return rs[1], nil + } + + return "", fmt.Errorf("unable to parse role arn %q", arn) +} + +func (b *KopsModelContext) LinkToIAMInstanceProfile(ig *kops.InstanceGroup) (*awstasks.IAMInstanceProfile, error) { + + name, err := b.IAMName(ig.Spec.Role) + if err != nil { + return nil, err + } + return &awstasks.IAMInstanceProfile{Name: &name}, nil } // SSHKeyName computes a unique SSH key name, combining the cluster name and the SSH public key fingerprint. diff --git a/tests/integration/custom_iam_role/id_rsa.pub b/tests/integration/custom_iam_role/id_rsa.pub new file mode 100755 index 0000000000000..81cb0127830e7 --- /dev/null +++ b/tests/integration/custom_iam_role/id_rsa.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQCtWu40XQo8dczLsCq0OWV+hxm9uV3WxeH9Kgh4sMzQxNtoU1pvW0XdjpkBesRKGoolfWeCLXWxpyQb1IaiMkKoz7MdhQ/6UKjMjP66aFWWp3pwD0uj0HuJ7tq4gKHKRYGTaZIRWpzUiANBrjugVgA+Sd7E/mYwc/DMXkIyRZbvhQ== diff --git a/tests/integration/custom_iam_role/in-v1alpha2.yaml b/tests/integration/custom_iam_role/in-v1alpha2.yaml new file mode 100644 index 0000000000000..32e97e1796b82 --- /dev/null +++ b/tests/integration/custom_iam_role/in-v1alpha2.yaml @@ -0,0 +1,82 @@ +apiVersion: kops/v1alpha2 +kind: Cluster +metadata: + creationTimestamp: 2017-01-01T00:00:00Z + name: custom-iam-role.example.com +spec: + api: + dns: {} + authProfile: + master: "arn:aws:iam::4222917490108:instance-profile/kops-custom-master-role" + node: "arn:aws:iam::422917490108:instance-profile/kops-custom-node-role" + channel: stable + cloudProvider: aws + configBase: memfs://tests/custom-iam-role.example.com + etcdClusters: + - etcdMembers: + - instanceGroup: master-us-test-1a + name: a + name: main + - etcdMembers: + - instanceGroup: master-us-test-1a + name: a + name: events + kubernetesApiAccess: + - 0.0.0.0/0 + kubernetesVersion: v1.6.4 + masterPublicName: api.custom-iam-role.example.com + networkCIDR: 172.20.0.0/16 + networking: + kubenet: {} + nonMasqueradeCIDR: 100.64.0.0/10 + roleCustomIamRoles: + Master: foo + Node: bar + sshAccess: + - 0.0.0.0/0 + subnets: + - cidr: 172.20.32.0/19 + name: us-test-1a + type: Public + zone: us-test-1a + topology: + dns: + type: Public + masters: public + nodes: public + +--- + +apiVersion: kops/v1alpha2 +kind: InstanceGroup +metadata: + creationTimestamp: 2017-01-01T00:00:00Z + labels: + kops.k8s.io/cluster: custom-iam-role.example.com + name: master-us-test-1a +spec: + image: kope.io/k8s-1.5-debian-jessie-amd64-hvm-ebs-2017-01-09 + machineType: m3.medium + maxSize: 1 + minSize: 1 + role: Master + subnets: + - us-test-1a + +--- + +apiVersion: kops/v1alpha2 +kind: InstanceGroup +metadata: + creationTimestamp: 2017-01-01T00:00:00Z + labels: + kops.k8s.io/cluster: custom-iam-role.example.com + name: nodes +spec: + image: kope.io/k8s-1.5-debian-jessie-amd64-hvm-ebs-2017-01-09 + machineType: t2.medium + maxSize: 2 + minSize: 2 + role: Node + subnets: + - us-test-1a diff --git a/tests/integration/custom_iam_role/kubernetes.tf b/tests/integration/custom_iam_role/kubernetes.tf new file mode 100644 index 0000000000000..10658ad70dca6 --- /dev/null +++ b/tests/integration/custom_iam_role/kubernetes.tf @@ -0,0 +1,352 @@ +output "cluster_name" { + value = "custom-iam-role.example.com" +} + +output "master_security_group_ids" { + value = ["${aws_security_group.masters-custom-iam-role-example-com.id}"] +} + +output "node_security_group_ids" { + value = ["${aws_security_group.nodes-custom-iam-role-example-com.id}"] +} + +output "node_subnet_ids" { + value = ["${aws_subnet.us-test-1a-custom-iam-role-example-com.id}"] +} + +output "region" { + value = "us-test-1" +} + +output "vpc_id" { + value = "${aws_vpc.custom-iam-role-example-com.id}" +} + +provider "aws" { + region = "us-test-1" +} + +resource "aws_autoscaling_group" "master-us-test-1a-masters-custom-iam-role-example-com" { + name = "master-us-test-1a.masters.custom-iam-role.example.com" + launch_configuration = "${aws_launch_configuration.master-us-test-1a-masters-custom-iam-role-example-com.id}" + max_size = 1 + min_size = 1 + vpc_zone_identifier = ["${aws_subnet.us-test-1a-custom-iam-role-example-com.id}"] + + tag = { + key = "KubernetesCluster" + value = "custom-iam-role.example.com" + propagate_at_launch = true + } + + tag = { + key = "Name" + value = "master-us-test-1a.masters.custom-iam-role.example.com" + propagate_at_launch = true + } + + tag = { + key = "k8s.io/role/master" + value = "1" + propagate_at_launch = true + } +} + +resource "aws_autoscaling_group" "nodes-custom-iam-role-example-com" { + name = "nodes.custom-iam-role.example.com" + launch_configuration = "${aws_launch_configuration.nodes-custom-iam-role-example-com.id}" + max_size = 2 + min_size = 2 + vpc_zone_identifier = ["${aws_subnet.us-test-1a-custom-iam-role-example-com.id}"] + + tag = { + key = "KubernetesCluster" + value = "custom-iam-role.example.com" + propagate_at_launch = true + } + + tag = { + key = "Name" + value = "nodes.custom-iam-role.example.com" + propagate_at_launch = true + } + + tag = { + key = "k8s.io/role/node" + value = "1" + propagate_at_launch = true + } +} + +resource "aws_ebs_volume" "a-etcd-events-custom-iam-role-example-com" { + availability_zone = "us-test-1a" + size = 20 + type = "gp2" + encrypted = false + + tags = { + KubernetesCluster = "custom-iam-role.example.com" + Name = "a.etcd-events.custom-iam-role.example.com" + "k8s.io/etcd/events" = "a/a" + "k8s.io/role/master" = "1" + } +} + +resource "aws_ebs_volume" "a-etcd-main-custom-iam-role-example-com" { + availability_zone = "us-test-1a" + size = 20 + type = "gp2" + encrypted = false + + tags = { + KubernetesCluster = "custom-iam-role.example.com" + Name = "a.etcd-main.custom-iam-role.example.com" + "k8s.io/etcd/main" = "a/a" + "k8s.io/role/master" = "1" + } +} + +resource "aws_internet_gateway" "custom-iam-role-example-com" { + vpc_id = "${aws_vpc.custom-iam-role-example-com.id}" + + tags = { + KubernetesCluster = "custom-iam-role.example.com" + Name = "custom-iam-role.example.com" + } +} + +resource "aws_key_pair" "kubernetes-custom-iam-role-example-com-c4a6ed9aa889b9e2c39cd663eb9c7157" { + key_name = "kubernetes.custom-iam-role.example.com-c4:a6:ed:9a:a8:89:b9:e2:c3:9c:d6:63:eb:9c:71:57" + public_key = "${file("${path.module}/data/aws_key_pair_kubernetes.custom-iam-role.example.com-c4a6ed9aa889b9e2c39cd663eb9c7157_public_key")}" +} + +resource "aws_launch_configuration" "master-us-test-1a-masters-custom-iam-role-example-com" { + name_prefix = "master-us-test-1a.masters.custom-iam-role.example.com-" + image_id = "ami-15000000" + instance_type = "m3.medium" + key_name = "${aws_key_pair.kubernetes-custom-iam-role-example-com-c4a6ed9aa889b9e2c39cd663eb9c7157.id}" + iam_instance_profile = "arn:aws:iam::4222917490108:instance-profile/kops-custom-master-role" + security_groups = ["${aws_security_group.masters-custom-iam-role-example-com.id}"] + associate_public_ip_address = true + user_data = "${file("${path.module}/data/aws_launch_configuration_master-us-test-1a.masters.custom-iam-role.example.com_user_data")}" + + root_block_device = { + volume_type = "gp2" + volume_size = 64 + delete_on_termination = true + } + + ephemeral_block_device = { + device_name = "/dev/sdc" + virtual_name = "ephemeral0" + } + + lifecycle = { + create_before_destroy = true + } +} + +resource "aws_launch_configuration" "nodes-custom-iam-role-example-com" { + name_prefix = "nodes.custom-iam-role.example.com-" + image_id = "ami-15000000" + instance_type = "t2.medium" + key_name = "${aws_key_pair.kubernetes-custom-iam-role-example-com-c4a6ed9aa889b9e2c39cd663eb9c7157.id}" + iam_instance_profile = "arn:aws:iam::422917490108:instance-profile/kops-custom-node-role" + security_groups = ["${aws_security_group.nodes-custom-iam-role-example-com.id}"] + associate_public_ip_address = true + user_data = "${file("${path.module}/data/aws_launch_configuration_nodes.custom-iam-role.example.com_user_data")}" + + root_block_device = { + volume_type = "gp2" + volume_size = 128 + delete_on_termination = true + } + + lifecycle = { + create_before_destroy = true + } +} + +resource "aws_route" "0-0-0-0--0" { + route_table_id = "${aws_route_table.custom-iam-role-example-com.id}" + destination_cidr_block = "0.0.0.0/0" + gateway_id = "${aws_internet_gateway.custom-iam-role-example-com.id}" +} + +resource "aws_route_table" "custom-iam-role-example-com" { + vpc_id = "${aws_vpc.custom-iam-role-example-com.id}" + + tags = { + KubernetesCluster = "custom-iam-role.example.com" + Name = "custom-iam-role.example.com" + } +} + +resource "aws_route_table_association" "us-test-1a-custom-iam-role-example-com" { + subnet_id = "${aws_subnet.us-test-1a-custom-iam-role-example-com.id}" + route_table_id = "${aws_route_table.custom-iam-role-example-com.id}" +} + +resource "aws_security_group" "masters-custom-iam-role-example-com" { + name = "masters.custom-iam-role.example.com" + vpc_id = "${aws_vpc.custom-iam-role-example-com.id}" + description = "Security group for masters" + + tags = { + KubernetesCluster = "custom-iam-role.example.com" + Name = "masters.custom-iam-role.example.com" + } +} + +resource "aws_security_group" "nodes-custom-iam-role-example-com" { + name = "nodes.custom-iam-role.example.com" + vpc_id = "${aws_vpc.custom-iam-role-example-com.id}" + description = "Security group for nodes" + + tags = { + KubernetesCluster = "custom-iam-role.example.com" + Name = "nodes.custom-iam-role.example.com" + } +} + +resource "aws_security_group_rule" "all-master-to-master" { + type = "ingress" + security_group_id = "${aws_security_group.masters-custom-iam-role-example-com.id}" + source_security_group_id = "${aws_security_group.masters-custom-iam-role-example-com.id}" + from_port = 0 + to_port = 0 + protocol = "-1" +} + +resource "aws_security_group_rule" "all-master-to-node" { + type = "ingress" + security_group_id = "${aws_security_group.nodes-custom-iam-role-example-com.id}" + source_security_group_id = "${aws_security_group.masters-custom-iam-role-example-com.id}" + from_port = 0 + to_port = 0 + protocol = "-1" +} + +resource "aws_security_group_rule" "all-node-to-node" { + type = "ingress" + security_group_id = "${aws_security_group.nodes-custom-iam-role-example-com.id}" + source_security_group_id = "${aws_security_group.nodes-custom-iam-role-example-com.id}" + from_port = 0 + to_port = 0 + protocol = "-1" +} + +resource "aws_security_group_rule" "https-external-to-master-0-0-0-0--0" { + type = "ingress" + security_group_id = "${aws_security_group.masters-custom-iam-role-example-com.id}" + from_port = 443 + to_port = 443 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] +} + +resource "aws_security_group_rule" "master-egress" { + type = "egress" + security_group_id = "${aws_security_group.masters-custom-iam-role-example-com.id}" + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] +} + +resource "aws_security_group_rule" "node-egress" { + type = "egress" + security_group_id = "${aws_security_group.nodes-custom-iam-role-example-com.id}" + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] +} + +resource "aws_security_group_rule" "node-to-master-tcp-1-4000" { + type = "ingress" + security_group_id = "${aws_security_group.masters-custom-iam-role-example-com.id}" + source_security_group_id = "${aws_security_group.nodes-custom-iam-role-example-com.id}" + from_port = 1 + to_port = 4000 + protocol = "tcp" +} + +resource "aws_security_group_rule" "node-to-master-tcp-4003-65535" { + type = "ingress" + security_group_id = "${aws_security_group.masters-custom-iam-role-example-com.id}" + source_security_group_id = "${aws_security_group.nodes-custom-iam-role-example-com.id}" + from_port = 4003 + to_port = 65535 + protocol = "tcp" +} + +resource "aws_security_group_rule" "node-to-master-udp-1-65535" { + type = "ingress" + security_group_id = "${aws_security_group.masters-custom-iam-role-example-com.id}" + source_security_group_id = "${aws_security_group.nodes-custom-iam-role-example-com.id}" + from_port = 1 + to_port = 65535 + protocol = "udp" +} + +resource "aws_security_group_rule" "ssh-external-to-master-0-0-0-0--0" { + type = "ingress" + security_group_id = "${aws_security_group.masters-custom-iam-role-example-com.id}" + from_port = 22 + to_port = 22 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] +} + +resource "aws_security_group_rule" "ssh-external-to-node-0-0-0-0--0" { + type = "ingress" + security_group_id = "${aws_security_group.nodes-custom-iam-role-example-com.id}" + from_port = 22 + to_port = 22 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] +} + +resource "aws_subnet" "us-test-1a-custom-iam-role-example-com" { + vpc_id = "${aws_vpc.custom-iam-role-example-com.id}" + cidr_block = "172.20.32.0/19" + availability_zone = "us-test-1a" + + tags = { + KubernetesCluster = "custom-iam-role.example.com" + Name = "us-test-1a.custom-iam-role.example.com" + "kubernetes.io/cluster/custom-iam-role.example.com" = "owned" + } +} + +resource "aws_vpc" "custom-iam-role-example-com" { + cidr_block = "172.20.0.0/16" + enable_dns_hostnames = true + enable_dns_support = true + + tags = { + KubernetesCluster = "custom-iam-role.example.com" + Name = "custom-iam-role.example.com" + "kubernetes.io/cluster/custom-iam-role.example.com" = "owned" + } +} + +resource "aws_vpc_dhcp_options" "custom-iam-role-example-com" { + domain_name = "us-test-1.compute.internal" + domain_name_servers = ["AmazonProvidedDNS"] + + tags = { + KubernetesCluster = "custom-iam-role.example.com" + Name = "custom-iam-role.example.com" + } +} + +resource "aws_vpc_dhcp_options_association" "custom-iam-role-example-com" { + vpc_id = "${aws_vpc.custom-iam-role-example-com.id}" + dhcp_options_id = "${aws_vpc_dhcp_options.custom-iam-role-example-com.id}" +} + +terraform = { + required_version = ">= 0.9.3" +} diff --git a/upup/pkg/fi/cloudup/awstasks/iaminstanceprofile.go b/upup/pkg/fi/cloudup/awstasks/iaminstanceprofile.go index e7e995cdb6300..50804ca185993 100644 --- a/upup/pkg/fi/cloudup/awstasks/iaminstanceprofile.go +++ b/upup/pkg/fi/cloudup/awstasks/iaminstanceprofile.go @@ -34,8 +34,9 @@ import ( type IAMInstanceProfile struct { Name *string Lifecycle *fi.Lifecycle - - ID *string + // Shared is set if this is a shared instance profile + Shared *bool + ID *string } var _ fi.CompareWithID = &IAMInstanceProfile{} @@ -93,9 +94,16 @@ func (e *IAMInstanceProfile) Run(c *fi.Context) error { return fi.DefaultDeltaRunMethod(e, c) } -func (s *IAMInstanceProfile) CheckChanges(a, e, changes *IAMInstanceProfile) error { +func (_ *IAMInstanceProfile) ShouldCreate(a, e, changes *IAMInstanceProfile) (bool, error) { + if fi.BoolValue(e.Shared) { + return false, nil + } + return true, nil +} + +func (_ *IAMInstanceProfile) CheckChanges(a, e, changes *IAMInstanceProfile) error { if a != nil { - if fi.StringValue(e.Name) == "" { + if fi.StringValue(e.Name) == "" && !fi.BoolValue(e.Shared) { return fi.RequiredField("Name") } } @@ -103,7 +111,11 @@ func (s *IAMInstanceProfile) CheckChanges(a, e, changes *IAMInstanceProfile) err } func (_ *IAMInstanceProfile) RenderAWS(t *awsup.AWSAPITarget, a, e, changes *IAMInstanceProfile) error { - if a == nil { + if fi.BoolValue(e.Shared) { + if a == nil { + return fmt.Errorf("instance role profile with id %q not found", fi.StringValue(e.ID)) + } + } else if a == nil { glog.V(2).Infof("Creating IAMInstanceProfile with Name:%q", *e.Name) request := &iam.CreateInstanceProfileInput{ @@ -154,6 +166,9 @@ func (_ *IAMInstanceProfile) RenderTerraform(t *terraform.TerraformTarget, a, e, } func (e *IAMInstanceProfile) TerraformLink() *terraform.Literal { + if fi.BoolValue(e.Shared) { + return terraform.LiteralFromStringValue(*e.ID) + } return terraform.LiteralProperty("aws_iam_instance_profile", *e.Name, "id") } @@ -163,5 +178,10 @@ func (_ *IAMInstanceProfile) RenderCloudformation(t *cloudformation.Cloudformati } func (e *IAMInstanceProfile) CloudformationLink() *cloudformation.Literal { + if fi.BoolValue(e.Shared) { + // FIXME no idea how to get this to work + glog.Warning("cf does not support custom iam profiles at this time") + return nil + } return cloudformation.Ref("AWS::IAM::InstanceProfile", *e.Name) } diff --git a/upup/pkg/fi/cloudup/awstasks/iaminstanceprofilerole.go b/upup/pkg/fi/cloudup/awstasks/iaminstanceprofilerole.go index 8436e0f8277a1..a8828965726b6 100644 --- a/upup/pkg/fi/cloudup/awstasks/iaminstanceprofilerole.go +++ b/upup/pkg/fi/cloudup/awstasks/iaminstanceprofilerole.go @@ -117,6 +117,9 @@ type terraformIAMInstanceProfile struct { } func (_ *IAMInstanceProfileRole) RenderTerraform(t *terraform.TerraformTarget, a, e, changes *IAMInstanceProfileRole) error { + if fi.BoolValue(e.InstanceProfile.Shared) { + return nil + } tf := &terraformIAMInstanceProfile{ Name: e.InstanceProfile.Name, Role: e.Role.TerraformLink(), @@ -131,6 +134,9 @@ type cloudformationIAMInstanceProfile struct { } func (_ *IAMInstanceProfileRole) RenderCloudformation(t *cloudformation.CloudformationTarget, a, e, changes *IAMInstanceProfileRole) error { + if fi.BoolValue(e.InstanceProfile.Shared) { + return nil + } cf := &cloudformationIAMInstanceProfile{ //Path: e.InstanceProfile.Name, Roles: []*cloudformation.Literal{e.Role.CloudformationLink()}, diff --git a/upup/pkg/fi/cloudup/awstasks/iamrole.go b/upup/pkg/fi/cloudup/awstasks/iamrole.go index 57f2327d9b939..1fd18ebb69886 100644 --- a/upup/pkg/fi/cloudup/awstasks/iamrole.go +++ b/upup/pkg/fi/cloudup/awstasks/iamrole.go @@ -194,6 +194,12 @@ type terraformIAMRole struct { } func (_ *IAMRole) RenderTerraform(t *terraform.TerraformTarget, a, e, changes *IAMRole) error { + + // TODO how can I do this better, but we are reusing the role + if e.RolePolicyDocument == nil { + return nil + } + policy, err := t.AddFile("aws_iam_role", *e.Name, "policy", e.RolePolicyDocument) if err != nil { return fmt.Errorf("error rendering RolePolicyDocument: %v", err) @@ -213,7 +219,11 @@ func (_ *IAMRole) RenderTerraform(t *terraform.TerraformTarget, a, e, changes *I } func (e *IAMRole) TerraformLink() *terraform.Literal { - return terraform.LiteralProperty("aws_iam_role", *e.Name, "name") + if e.RolePolicyDocument != nil { + return terraform.LiteralProperty("aws_iam_role", *e.Name, "name") + } + + return terraform.LiteralFromStringValue(*e.ID) } type cloudformationIAMRole struct { @@ -222,6 +232,10 @@ type cloudformationIAMRole struct { } func (_ *IAMRole) RenderCloudformation(t *cloudformation.CloudformationTarget, a, e, changes *IAMRole) error { + if e.RolePolicyDocument == nil { + // TODO fix CF for re-using iam profiles + return fmt.Errorf("at this time not having a role document is not supported by cf") + } jsonString, err := e.RolePolicyDocument.AsBytes() if err != nil { return err @@ -242,5 +256,10 @@ func (_ *IAMRole) RenderCloudformation(t *cloudformation.CloudformationTarget, a } func (e *IAMRole) CloudformationLink() *cloudformation.Literal { + if e.RolePolicyDocument == nil { + // TODO fix CF for re-using iam profiles + glog.Warningf("at this time not having a role document is not supported by cf") + return nil + } return cloudformation.Ref("AWS::IAM::Role", *e.Name) }