Skip to content

Commit

Permalink
Enabling JSON output for Terraform instead of writing the HCL syntax …
Browse files Browse the repository at this point in the history
…tf file. JSON syntax is officially supported in 0.12 and a terraform version requirement will be set. For previous installations you need to delete the .tf file by hand. JSON generation will fail if kubernetes.tf is present.

Added Integration Test using minimal test setup

Added documentation. For terraform 0.12 support the resource names need to be changed still
  • Loading branch information
mccare committed Jan 9, 2020
1 parent 1b8b074 commit 4e622a0
Show file tree
Hide file tree
Showing 8 changed files with 671 additions and 44 deletions.
98 changes: 59 additions & 39 deletions cmd/kops/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,20 +51,20 @@ const updateClusterTestBase = "../../tests/integration/update_cluster/"

// 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", "minimal", "v1alpha0", false, 1, true, false, nil)
runTestAWS(t, "minimal.example.com", "minimal", "v1alpha1", false, 1, true, false, nil)
runTestAWS(t, "minimal.example.com", "minimal", "v1alpha2", false, 1, true, false, nil)
runTestAWS(t, "minimal.example.com", "minimal", "v1alpha0", false, 1, true, false, nil, false)
runTestAWS(t, "minimal.example.com", "minimal", "v1alpha1", false, 1, true, false, nil, false)
runTestAWS(t, "minimal.example.com", "minimal", "v1alpha2", false, 1, true, false, nil, false)
}

// TestRestrictAccess runs the test on a simple SG configuration, similar to kops create cluster minimal.example.com --ssh-access=$(IPS) --admin-access=$(IPS) --master-count=3
func TestRestrictAccess(t *testing.T) {
runTestAWS(t, "restrictaccess.example.com", "restrict_access", "v1alpha2", false, 1, true, false, nil)
runTestAWS(t, "restrictaccess.example.com", "restrict_access", "v1alpha2", false, 1, true, false, nil, false)
}

// 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", "ha", "v1alpha1", false, 3, true, false, nil)
runTestAWS(t, "ha.example.com", "ha", "v1alpha2", false, 3, true, false, nil)
runTestAWS(t, "ha.example.com", "ha", "v1alpha1", false, 3, true, false, nil, false)
runTestAWS(t, "ha.example.com", "ha", "v1alpha2", false, 3, true, false, nil, false)
}

// TestHighAvailabilityGCE runs the test on a simple HA GCE configuration, similar to kops create cluster ha-gce.example.com
Expand All @@ -75,15 +75,15 @@ func TestHighAvailabilityGCE(t *testing.T) {

// 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", "complex", "v1alpha2", false, 1, true, false, nil)
runTestAWS(t, "complex.example.com", "complex", "legacy-v1alpha2", false, 1, true, false, nil)
runTestAWS(t, "complex.example.com", "complex", "v1alpha2", false, 1, true, false, nil, false)
runTestAWS(t, "complex.example.com", "complex", "legacy-v1alpha2", false, 1, true, false, nil, false)

runTestCloudformation(t, "complex.example.com", "complex", "v1alpha2", false, nil)
}

// TestCrossZone tests that the cross zone setting on the API ELB is set properly
func TestCrossZone(t *testing.T) {
runTestAWS(t, "crosszone.example.com", "api_elb_cross_zone", "v1alpha2", false, 1, true, false, nil)
runTestAWS(t, "crosszone.example.com", "api_elb_cross_zone", "v1alpha2", false, 1, true, false, nil, false)
}

// TestMinimalCloudformation runs the test on a minimum configuration, similar to kops create cluster minimal.example.com --zones us-west-1a
Expand All @@ -99,7 +99,7 @@ func TestExistingIAMCloudformation(t *testing.T) {

// TestExistingSG runs the test with existing Security Group, similar to kops create cluster minimal.example.com --zones us-west-1a
func TestExistingSG(t *testing.T) {
runTestAWS(t, "existingsg.example.com", "existing_sg", "v1alpha2", false, 3, true, false, nil)
runTestAWS(t, "existingsg.example.com", "existing_sg", "v1alpha2", false, 3, true, false, nil, false)
}

// TestAdditionalUserData runs the test on passing additional user-data to an instance at bootstrap.
Expand All @@ -109,83 +109,93 @@ func TestAdditionalUserData(t *testing.T) {

// TestBastionAdditionalUserData runs the test on passing additional user-data to a bastion instance group
func TestBastionAdditionalUserData(t *testing.T) {
runTestAWS(t, "bastionuserdata.example.com", "bastionadditional_user-data", "v1alpha2", true, 1, true, false, nil)
runTestAWS(t, "bastionuserdata.example.com", "bastionadditional_user-data", "v1alpha2", true, 1, true, false, nil, false)
}

// TestMinimal_JSON runs the test on a minimal data set and outputs JSON
func TestMinimal_json(t *testing.T) {
featureflag.ParseFlags("+TerraformJSON")
unsetFeaureFlag := func() {
featureflag.ParseFlags("-TerraformJSON")
}
defer unsetFeaureFlag()
runTestAWS(t, "minimal-json.example.com", "minimal-json", "v1alpha0", false, 1, true, false, nil, true)
}

// 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", "minimal-141", "v1alpha0", false, 1, true, false, nil)
runTestAWS(t, "minimal-141.example.com", "minimal-141", "v1alpha0", false, 1, true, false, nil, false)
}

// TestPrivateWeave runs the test on a configuration with private topology, weave networking
func TestPrivateWeave(t *testing.T) {
runTestAWS(t, "privateweave.example.com", "privateweave", "v1alpha1", true, 1, true, false, nil)
runTestAWS(t, "privateweave.example.com", "privateweave", "v1alpha2", true, 1, true, false, nil)
runTestAWS(t, "privateweave.example.com", "privateweave", "v1alpha1", true, 1, true, false, nil, false)
runTestAWS(t, "privateweave.example.com", "privateweave", "v1alpha2", true, 1, true, false, nil, false)
}

// TestPrivateFlannel runs the test on a configuration with private topology, flannel networking
func TestPrivateFlannel(t *testing.T) {
runTestAWS(t, "privateflannel.example.com", "privateflannel", "v1alpha1", true, 1, true, false, nil)
runTestAWS(t, "privateflannel.example.com", "privateflannel", "v1alpha2", true, 1, true, false, nil)
runTestAWS(t, "privateflannel.example.com", "privateflannel", "v1alpha1", true, 1, true, false, nil, false)
runTestAWS(t, "privateflannel.example.com", "privateflannel", "v1alpha2", true, 1, true, false, nil, false)
}

// TestPrivateCalico runs the test on a configuration with private topology, calico networking
func TestPrivateCalico(t *testing.T) {
runTestAWS(t, "privatecalico.example.com", "privatecalico", "v1alpha1", true, 1, true, false, nil)
runTestAWS(t, "privatecalico.example.com", "privatecalico", "v1alpha2", true, 1, true, false, nil)
runTestAWS(t, "privatecalico.example.com", "privatecalico", "v1alpha1", true, 1, true, false, nil, false)
runTestAWS(t, "privatecalico.example.com", "privatecalico", "v1alpha2", true, 1, true, false, nil, false)
runTestCloudformation(t, "privatecalico.example.com", "privatecalico", "v1alpha2", true, nil)
}

// TestPrivateCanal runs the test on a configuration with private topology, canal networking
func TestPrivateCanal(t *testing.T) {
runTestAWS(t, "privatecanal.example.com", "privatecanal", "v1alpha1", true, 1, true, false, nil)
runTestAWS(t, "privatecanal.example.com", "privatecanal", "v1alpha2", true, 1, true, false, nil)
runTestAWS(t, "privatecanal.example.com", "privatecanal", "v1alpha1", true, 1, true, false, nil, false)
runTestAWS(t, "privatecanal.example.com", "privatecanal", "v1alpha2", true, 1, true, false, nil, false)
}

// TestPrivateKopeio runs the test on a configuration with private topology, kopeio networking
func TestPrivateKopeio(t *testing.T) {
runTestAWS(t, "privatekopeio.example.com", "privatekopeio", "v1alpha2", true, 1, true, false, nil)
runTestAWS(t, "privatekopeio.example.com", "privatekopeio", "v1alpha2", true, 1, true, false, nil, false)
}

// TestUnmanaged is a test where all the subnets opt-out of route management
func TestUnmanaged(t *testing.T) {
runTestAWS(t, "unmanaged.example.com", "unmanaged", "v1alpha2", true, 1, true, false, nil)
runTestAWS(t, "unmanaged.example.com", "unmanaged", "v1alpha2", true, 1, true, false, nil, false)
}

// TestPrivateSharedSubnet runs the test on a configuration with private topology & shared subnets
func TestPrivateSharedSubnet(t *testing.T) {
runTestAWS(t, "private-shared-subnet.example.com", "private-shared-subnet", "v1alpha2", true, 1, true, false, nil)
runTestAWS(t, "private-shared-subnet.example.com", "private-shared-subnet", "v1alpha2", true, 1, true, false, nil, false)
}

// TestPrivateDns1 runs the test on a configuration with private topology, private dns
func TestPrivateDns1(t *testing.T) {
runTestAWS(t, "privatedns1.example.com", "privatedns1", "v1alpha2", true, 1, true, false, nil)
runTestAWS(t, "privatedns1.example.com", "privatedns1", "v1alpha2", true, 1, true, false, nil, false)
}

// TestPrivateDns2 runs the test on a configuration with private topology, private dns, extant vpc
func TestPrivateDns2(t *testing.T) {
runTestAWS(t, "privatedns2.example.com", "privatedns2", "v1alpha2", true, 1, true, false, nil)
runTestAWS(t, "privatedns2.example.com", "privatedns2", "v1alpha2", true, 1, true, false, nil, false)
}

// TestSharedSubnet runs the test on a configuration with a shared subnet (and VPC)
func TestSharedSubnet(t *testing.T) {
runTestAWS(t, "sharedsubnet.example.com", "shared_subnet", "v1alpha2", false, 1, true, false, nil)
runTestAWS(t, "sharedsubnet.example.com", "shared_subnet", "v1alpha2", false, 1, true, false, nil, false)
}

// TestSharedVPC runs the test on a configuration with a shared VPC
func TestSharedVPC(t *testing.T) {
runTestAWS(t, "sharedvpc.example.com", "shared_vpc", "v1alpha2", false, 1, true, false, nil)
runTestAWS(t, "sharedvpc.example.com", "shared_vpc", "v1alpha2", false, 1, true, false, nil, false)
}

// TestExistingIAM runs the test on a configuration with existing IAM instance profiles
func TestExistingIAM(t *testing.T) {
lifecycleOverrides := []string{"IAMRole=ExistsAndWarnIfChanges", "IAMRolePolicy=ExistsAndWarnIfChanges", "IAMInstanceProfileRole=ExistsAndWarnIfChanges"}
runTestAWS(t, "existing-iam.example.com", "existing_iam", "v1alpha2", false, 3, false, false, lifecycleOverrides)
runTestAWS(t, "existing-iam.example.com", "existing_iam", "v1alpha2", false, 3, false, false, lifecycleOverrides, false)
}

// TestAdditionalCIDR runs the test on a configuration with a shared VPC
func TestAdditionalCIDR(t *testing.T) {
runTestAWS(t, "additionalcidr.example.com", "additional_cidr", "v1alpha3", false, 3, true, false, nil)
runTestAWS(t, "additionalcidr.example.com", "additional_cidr", "v1alpha3", false, 3, true, false, nil, false)
runTestCloudformation(t, "additionalcidr.example.com", "additional_cidr", "v1alpha2", false, nil)
}

Expand All @@ -195,7 +205,7 @@ func TestPhaseNetwork(t *testing.T) {
}

func TestExternalLoadBalancer(t *testing.T) {
runTestAWS(t, "externallb.example.com", "externallb", "v1alpha2", false, 1, true, false, nil)
runTestAWS(t, "externallb.example.com", "externallb", "v1alpha2", false, 1, true, false, nil, false)
runTestCloudformation(t, "externallb.example.com", "externallb", "v1alpha2", false, nil)
}

Expand All @@ -214,13 +224,13 @@ func TestPhaseCluster(t *testing.T) {

// TestMixedInstancesASG tests ASGs using a mixed instance policy
func TestMixedInstancesASG(t *testing.T) {
runTestAWS(t, "mixedinstances.example.com", "mixed_instances", "v1alpha2", false, 3, true, true, nil)
runTestAWS(t, "mixedinstances.example.com", "mixed_instances", "v1alpha2", false, 3, true, true, nil, false)
runTestCloudformation(t, "mixedinstances.example.com", "mixed_instances", "v1alpha2", false, nil)
}

// TestMixedInstancesSpotASG tests ASGs using a mixed instance policy and spot instances
func TestMixedInstancesSpotASG(t *testing.T) {
runTestAWS(t, "mixedinstances.example.com", "mixed_instances_spot", "v1alpha2", false, 3, true, true, nil)
runTestAWS(t, "mixedinstances.example.com", "mixed_instances_spot", "v1alpha2", false, 3, true, true, nil, false)
runTestCloudformation(t, "mixedinstances.example.com", "mixed_instances_spot", "v1alpha2", false, nil)
}

Expand All @@ -229,7 +239,7 @@ func TestContainerdCloudformation(t *testing.T) {
runTestCloudformation(t, "containerd.example.com", "containerd-cloudformation", "v1alpha2", false, nil)
}

func runTest(t *testing.T, h *testutils.IntegrationTestHarness, clusterName string, srcDir string, version string, private bool, zones int, expectedDataFilenames []string, tfFileName string, phase *cloudup.Phase, lifecycleOverrides []string) {
func runTest(t *testing.T, h *testutils.IntegrationTestHarness, clusterName string, srcDir string, version string, private bool, zones int, expectedDataFilenames []string, tfFileName string, expectedTfFileName string, phase *cloudup.Phase, lifecycleOverrides []string) {
var stdout bytes.Buffer

srcDir = updateClusterTestBase + srcDir
Expand All @@ -241,6 +251,10 @@ func runTest(t *testing.T, h *testutils.IntegrationTestHarness, clusterName stri
testDataTFPath = tfFileName
}

if expectedTfFileName != "" {
actualTFPath = expectedTfFileName
}

factoryOptions := &util.FactoryOptions{}
factoryOptions.RegistryPath = "memfs://tests"

Expand Down Expand Up @@ -303,10 +317,10 @@ func runTest(t *testing.T, h *testutils.IntegrationTestHarness, clusterName stri
sort.Strings(fileNames)

actualFilenames := strings.Join(fileNames, ",")
expectedFilenames := "kubernetes.tf"
expectedFilenames := actualTFPath

if len(expectedDataFilenames) > 0 {
expectedFilenames = "data,kubernetes.tf"
expectedFilenames = "data," + actualTFPath
}

if actualFilenames != expectedFilenames {
Expand Down Expand Up @@ -383,10 +397,15 @@ 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, expectPolicies bool, launchTemplate bool, lifecycleOverrides []string) {
func runTestAWS(t *testing.T, clusterName string, srcDir string, version string, private bool, zones int, expectPolicies bool, launchTemplate bool, lifecycleOverrides []string, jsonOutput bool) {
tfFileName := ""
h := testutils.NewIntegrationTestHarness(t)
defer h.Close()

if jsonOutput {
tfFileName = "kubernetes.tf.json"
}

h.MockKopsVersion("1.15.0")
h.SetupMockAWS()

Expand Down Expand Up @@ -427,7 +446,8 @@ func runTestAWS(t *testing.T, clusterName string, srcDir string, version string,
if srcDir == "bastionadditional_user-data" {
expectedFilenames = append(expectedFilenames, "aws_launch_configuration_bastion."+clusterName+"_user_data")
}
runTest(t, h, clusterName, srcDir, version, private, zones, expectedFilenames, "", nil, lifecycleOverrides)

runTest(t, h, clusterName, srcDir, version, private, zones, expectedFilenames, tfFileName, tfFileName, nil, lifecycleOverrides)
}

func runTestPhase(t *testing.T, clusterName string, srcDir string, version string, private bool, zones int, phase cloudup.Phase) {
Expand Down Expand Up @@ -473,7 +493,7 @@ func runTestPhase(t *testing.T, clusterName string, srcDir string, version strin
}
}

runTest(t, h, clusterName, srcDir, version, private, zones, expectedFilenames, tfFileName, &phase, nil)
runTest(t, h, clusterName, srcDir, version, private, zones, expectedFilenames, tfFileName, "", &phase, nil)
}

func runTestGCE(t *testing.T, clusterName string, srcDir string, version string, private bool, zones int) {
Expand Down Expand Up @@ -502,7 +522,7 @@ func runTestGCE(t *testing.T, clusterName string, srcDir string, version string,
expectedFilenames = append(expectedFilenames, prefix+"kops-k8s-io-instance-group-name")
}

runTest(t, h, clusterName, srcDir, version, private, zones, expectedFilenames, "", nil, nil)
runTest(t, h, clusterName, srcDir, version, private, zones, expectedFilenames, "", "", nil, nil)
}

func runTestCloudformation(t *testing.T, clusterName string, srcDir string, version string, private bool, lifecycleOverrides []string) {
Expand Down
1 change: 1 addition & 0 deletions docs/advanced/experimental.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ The following experimental features are currently available:
* `+Spotinst` - Enables the use of the Spotinst cloud provider
* `+SpotinstOcean` - Enables the use of Spotinst Ocean instance groups
* `+SkipEtcdVersionCheck` - Bypasses the check that etcd-manager is using a supported etcd version
* `+TerraformJSON` - Produce kubernetes.ts.json file instead of writing HCL v1 syntax. Can be consumed by terraform 0.12
13 changes: 13 additions & 0 deletions docs/terraform.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,3 +159,16 @@ $ terraform apply
```

You should still run `kops delete cluster ${CLUSTER_NAME}`, to remove the kops cluster specification and any dynamically created Kubernetes resources (ELBs or volumes), but under this workaround also to remove the primary ELB volumes from the `proto` phase.

#### Terraform JSON output

With terraform 0.12 JSON is now officially supported as configuration language. To enable JSON output instead of HCLv1 output you need to enable it through a feature flag.
```
export KOPS_FEATURE_FLAGS=TerraformJSON
kops update cluster .....
```

This is an alternative to of using terraforms own configuration syntax HCL. Be sure to delete the existing kubernetes.tf file. Terraform will otherwise use both and then complain.

Kops will require terraform 0.12 for JSON configuration. Inofficially (partially) it was also supported with terraform 0.11, so you can try and remove the `required_version` in `kubernetes.tf.json`.

2 changes: 2 additions & 0 deletions pkg/featureflag/featureflag.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ var (
VSphereCloudProvider = New("VSphereCloudProvider", Bool(false))
// SkipEtcdVersionCheck will bypass the check that etcd-manager is using a supported etcd version
SkipEtcdVersionCheck = New("SkipEtcdVersionCheck", Bool(false))
// Enable terraform JSON output instead of hcl output. JSON output can be also parsed by terraform 0.12
TerraformJSON = New("TerraformJSON", Bool(false))
)

// FeatureFlag defines a feature flag
Expand Down
1 change: 1 addition & 0 deletions tests/integration/update_cluster/minimal-json/id_rsa.pub
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQCtWu40XQo8dczLsCq0OWV+hxm9uV3WxeH9Kgh4sMzQxNtoU1pvW0XdjpkBesRKGoolfWeCLXWxpyQb1IaiMkKoz7MdhQ/6UKjMjP66aFWWp3pwD0uj0HuJ7tq4gKHKRYGTaZIRWpzUiANBrjugVgA+Sd7E/mYwc/DMXkIyRZbvhQ==
76 changes: 76 additions & 0 deletions tests/integration/update_cluster/minimal-json/in-v1alpha0.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
apiVersion: kops.k8s.io/v1alpha1
kind: Cluster
metadata:
creationTimestamp: "2016-12-10T22:42:27Z"
name: minimal-json.example.com
spec:
adminAccess:
- 0.0.0.0/0
channel: stable
cloudProvider: aws
configBase: memfs://clusters.example.com/minimal-json.example.com
etcdClusters:
- etcdMembers:
- name: us-test-1a
zone: us-test-1a
name: main
- etcdMembers:
- name: us-test-1a
zone: us-test-1a
name: events
kubernetesVersion: v1.14.0
masterInternalName: api.internal.minimal-json.example.com
masterPublicName: api.minimal-json.example.com
networkCIDR: 172.20.0.0/16
networking:
kubenet: {}
nonMasqueradeCIDR: 100.64.0.0/10
topology:
bastion:
idleTimeout: 120
machineType: t2.medium
masters: public
nodes: public
zones:
- cidr: 172.20.32.0/19
name: us-test-1a

---

apiVersion: kops.k8s.io/v1alpha1
kind: InstanceGroup
metadata:
creationTimestamp: "2016-12-10T22:42:28Z"
name: nodes
labels:
kops.k8s.io/cluster: minimal-json.example.com
spec:
associatePublicIp: true
image: kope.io/k8s-1.4-debian-jessie-amd64-hvm-ebs-2016-10-21
machineType: t2.medium
maxSize: 2
minSize: 2
role: Node
zones:
- us-test-1a

---

apiVersion: kops.k8s.io/v1alpha1
kind: InstanceGroup
metadata:
creationTimestamp: "2016-12-10T22:42:28Z"
name: master-us-test-1a
labels:
kops.k8s.io/cluster: minimal-json.example.com
spec:
associatePublicIp: true
image: kope.io/k8s-1.4-debian-jessie-amd64-hvm-ebs-2016-10-21
machineType: m3.medium
maxSize: 1
minSize: 1
role: Master
zones:
- us-test-1a


Loading

0 comments on commit 4e622a0

Please sign in to comment.