From 8cc0239f99d5f7ffbe6cf88f740e20f84b333a85 Mon Sep 17 00:00:00 2001 From: John Murret Date: Tue, 13 Sep 2022 11:29:08 -0600 Subject: [PATCH] Add hashicups to upgrade functionality (#9) * Add Ability to install an HCP self-managed cluster (#8) * Add global.cloud to values.yaml * Map global.cloud.secreeName to environment variables and hcl in command for server container. * Adding cloud preset and validation for it. * add parsing gnm response to struct. * Added functionality and unit tests for SaveSecretsFromBootstrapConfig() * Added functionality and unit tests for GetHelmConfigWithMapSecretNames * Added functionality and unit tests for FetchAgentBootstrapConfig() * Rename cloud_preset_installer to cloud_preset_helper * hooked preset installer helper to install command. have unit test that works like acceptance test. passing. * changing code to work with the certs that get generated from HCP. affects setting -tls-server-name on get-auto-encrypt-client-ca, server-acl-init, and the acl-init init containers for clients. * Adding CLI Output changes. * Moving and consolidating cloud preset files * Moved preset templates to implementations of a Preset interface * removing upgrade tests * change context.TODO() to context.Background() * docstrings * fixing description for preset flag to properly show the list ofvalid presets. * Renaming coud_prset.go to cloud_set.go * refactor out common logic from the local getPreset functions in install and uninstall commands. * upgrade helm and k8s deps related to customize error. * refactor to use hcp-sdk * removing unused vars * updated based on latest specs. * Refactor usage of sdk client so that it properly picks up environment variables and starts oauth flow. * Make proper use of the resourceid to supply the BootstrapParams. Add the ability to pass in an http client to the install command and the CloudPreset so that TLS can be used with httptest mock server since tls is enforced for the oauth request in the hcp-sdk. * updated to latest hcp-sdk-go-internal version * include HCP_AUTH_URL and HCP_API_HOST in configuring server-statefulset * Adding comment to server-statefulset and commenting out test * update comment to correct hcp-go-sdk * update bats test comments for -tls-server-name * get rid of cli lint error * removing manual test * Apply suggestions from code review Co-authored-by: Kyle Schochenmaier * updating new secrets to not have quotes. using os.Unsetenv in tests * adding global.cloud.enabled * adding space in values.yaml * Apply suggestions from code review Co-authored-by: Iryna Shustava Co-authored-by: Kyle Schochenmaier * fixing test in install_test.go to have cleaner simpler logic around expecting errors. * update function comments in cloud_preset.go * updated conditional logic on server-statefulset.yaml to also look for cloud secret name in addition to cloud enabled when setting the cloud stanza * updated getDeepyCopyOfValidBootstrapConfig() to get DeepCopy.... * removing unused test logic Co-authored-by: Kyle Schochenmaier Co-authored-by: Iryna Shustava * CHANGELOG: formatting and missing links (#1467) * Omit non-IP defined endpoints from clusters (#1452) * Omit non-IP defined endpoints from clusters * Improve perf with regex * Use ParseIP instead of RegEx * Add test for parseClusters * Update the reference to cni package to the current on main. (#1472) * update Kubernetes versions throughout CI (#1460) * update Kube versions throughout CI so nightlies run against supported versions of Kubernetes. * release 0.48.0 (#1473) * release 0.48.0 * update envoy version to 1.23.1 * put main back into dev (#1476) * Add Ability to install an HCP self-managed cluster (#8) * Add global.cloud to values.yaml * Map global.cloud.secreeName to environment variables and hcl in command for server container. * Adding cloud preset and validation for it. * add parsing gnm response to struct. * Added functionality and unit tests for SaveSecretsFromBootstrapConfig() * Added functionality and unit tests for GetHelmConfigWithMapSecretNames * Added functionality and unit tests for FetchAgentBootstrapConfig() * Rename cloud_preset_installer to cloud_preset_helper * hooked preset installer helper to install command. have unit test that works like acceptance test. passing. * changing code to work with the certs that get generated from HCP. affects setting -tls-server-name on get-auto-encrypt-client-ca, server-acl-init, and the acl-init init containers for clients. * Adding CLI Output changes. * Moving and consolidating cloud preset files * Moved preset templates to implementations of a Preset interface * removing upgrade tests * change context.TODO() to context.Background() * docstrings * fixing description for preset flag to properly show the list ofvalid presets. * Renaming coud_prset.go to cloud_set.go * refactor out common logic from the local getPreset functions in install and uninstall commands. * upgrade helm and k8s deps related to customize error. * refactor to use hcp-sdk * removing unused vars * updated based on latest specs. * Refactor usage of sdk client so that it properly picks up environment variables and starts oauth flow. * Make proper use of the resourceid to supply the BootstrapParams. Add the ability to pass in an http client to the install command and the CloudPreset so that TLS can be used with httptest mock server since tls is enforced for the oauth request in the hcp-sdk. * updated to latest hcp-sdk-go-internal version * include HCP_AUTH_URL and HCP_API_HOST in configuring server-statefulset * Adding comment to server-statefulset and commenting out test * update comment to correct hcp-go-sdk * update bats test comments for -tls-server-name * get rid of cli lint error * removing manual test * Apply suggestions from code review Co-authored-by: Kyle Schochenmaier * updating new secrets to not have quotes. using os.Unsetenv in tests * adding global.cloud.enabled * adding space in values.yaml * Apply suggestions from code review Co-authored-by: Iryna Shustava Co-authored-by: Kyle Schochenmaier * fixing test in install_test.go to have cleaner simpler logic around expecting errors. * update function comments in cloud_preset.go * updated conditional logic on server-statefulset.yaml to also look for cloud secret name in addition to cloud enabled when setting the cloud stanza * updated getDeepyCopyOfValidBootstrapConfig() to get DeepCopy.... * removing unused test logic Co-authored-by: Kyle Schochenmaier Co-authored-by: Iryna Shustava * Add global.cloud to values.yaml * Map global.cloud.secreeName to environment variables and hcl in command for server container. * Adding cloud preset and validation for it. * add parsing gnm response to struct. * Added functionality and unit tests for SaveSecretsFromBootstrapConfig() * Added functionality and unit tests for GetHelmConfigWithMapSecretNames * Added functionality and unit tests for FetchAgentBootstrapConfig() * Rename cloud_preset_installer to cloud_preset_helper * hooked preset installer helper to install command. have unit test that works like acceptance test. passing. * changing code to work with the certs that get generated from HCP. affects setting -tls-server-name on get-auto-encrypt-client-ca, server-acl-init, and the acl-init init containers for clients. * Adding CLI Output changes. * Moving and consolidating cloud preset files * Moved preset templates to implementations of a Preset interface * removing upgrade tests * change context.TODO() to context.Background() * docstrings * fixing description for preset flag to properly show the list ofvalid presets. * Renaming coud_prset.go to cloud_set.go * refactor out common logic from the local getPreset functions in install and uninstall commands. * upgrade helm and k8s deps related to customize error. * refactor to use hcp-sdk * removing unused vars * updated based on latest specs. * Refactor usage of sdk client so that it properly picks up environment variables and starts oauth flow. * Make proper use of the resourceid to supply the BootstrapParams. Add the ability to pass in an http client to the install command and the CloudPreset so that TLS can be used with httptest mock server since tls is enforced for the oauth request in the hcp-sdk. * updated to latest hcp-sdk-go-internal version * include HCP_AUTH_URL and HCP_API_HOST in configuring server-statefulset * Adding comment to server-statefulset and commenting out test * update comment to correct hcp-go-sdk * update bats test comments for -tls-server-name * get rid of cli lint error * removing manual test * Apply suggestions from code review Co-authored-by: Kyle Schochenmaier * updating new secrets to not have quotes. using os.Unsetenv in tests * adding global.cloud.enabled * adding space in values.yaml * Apply suggestions from code review Co-authored-by: Iryna Shustava Co-authored-by: Kyle Schochenmaier * fixing test in install_test.go to have cleaner simpler logic around expecting errors. * update function comments in cloud_preset.go * updated conditional logic on server-statefulset.yaml to also look for cloud secret name in addition to cloud enabled when setting the cloud stanza * updated getDeepyCopyOfValidBootstrapConfig() to get DeepCopy.... * removing unused test logic * Adding upgrade functionality for cloud preset. * remove manual upgrade test * Embed hashicups demo helm chart and expose demo falg to install it. * fixing rebase conflicts in install.go * rename demo preset to quickstart preset * adding missing quickstart.go file * enable handling dry runof both consul and consul demo app. * modify hashicups deployments so that theyhave the consul.hashicorp.com/connect-inject annotation set to true so that they are automatically opted into the service mesh * making namespace dynamic in demo helm charts. * name for demo helm chart to consul-demo * basic uninstall working * refactor install and uninstall to re-use functions and components. * replace instance of Consul Demo with use of constant. * using --namespace instead of -n on port forwad command. capitalizing Accessing Consul Demo Applidation UI. * removing remnant test * correcting mergeconflict * removing temporary test * tests for install * fix issue bypassing the correct release name for consul-demo * use interface and mocks for helm actions and add tests. * Adding tests for install and uninstall * fixing rebasing loss of paren * added notes hashicup helm docs * Add tests for upgrade command * got basic upgrade tests passing needs refactoring * logic works with lots of duplications that need to be refactored. * updating install and uninstall test with mock assertions. * test terminal output. refactor tests into sub tests * refactored uninstall tests in sub tests * added tests for install errors * adding error tests to uninstall * adding error tests to upgrade * making header output consistent across install, upgrade, and uninstall * moving functionality to release.go so that install and upgrade commands can both install demo app if needed. * adding docstrings to structs and functions. * add docstring to InstallHelmReleaseOptions. * Adding tests for helm install and helm upgrade. * adding test for successful consul upgrade but failed demo upgrade. * adding helm install error tests. * adding helm upgrade error tests. * fixing linting error * Add Ability to install an HCP self-managed cluster (#8) * Add global.cloud to values.yaml * Map global.cloud.secreeName to environment variables and hcl in command for server container. * Adding cloud preset and validation for it. * add parsing gnm response to struct. * Added functionality and unit tests for SaveSecretsFromBootstrapConfig() * Added functionality and unit tests for GetHelmConfigWithMapSecretNames * Added functionality and unit tests for FetchAgentBootstrapConfig() * Rename cloud_preset_installer to cloud_preset_helper * hooked preset installer helper to install command. have unit test that works like acceptance test. passing. * changing code to work with the certs that get generated from HCP. affects setting -tls-server-name on get-auto-encrypt-client-ca, server-acl-init, and the acl-init init containers for clients. * Adding CLI Output changes. * Moving and consolidating cloud preset files * Moved preset templates to implementations of a Preset interface * removing upgrade tests * change context.TODO() to context.Background() * docstrings * fixing description for preset flag to properly show the list ofvalid presets. * Renaming coud_prset.go to cloud_set.go * refactor out common logic from the local getPreset functions in install and uninstall commands. * upgrade helm and k8s deps related to customize error. * refactor to use hcp-sdk * removing unused vars * updated based on latest specs. * Refactor usage of sdk client so that it properly picks up environment variables and starts oauth flow. * Make proper use of the resourceid to supply the BootstrapParams. Add the ability to pass in an http client to the install command and the CloudPreset so that TLS can be used with httptest mock server since tls is enforced for the oauth request in the hcp-sdk. * updated to latest hcp-sdk-go-internal version * include HCP_AUTH_URL and HCP_API_HOST in configuring server-statefulset * Adding comment to server-statefulset and commenting out test * update comment to correct hcp-go-sdk * update bats test comments for -tls-server-name * get rid of cli lint error * removing manual test * Apply suggestions from code review Co-authored-by: Kyle Schochenmaier * updating new secrets to not have quotes. using os.Unsetenv in tests * adding global.cloud.enabled * adding space in values.yaml * Apply suggestions from code review Co-authored-by: Iryna Shustava Co-authored-by: Kyle Schochenmaier * fixing test in install_test.go to have cleaner simpler logic around expecting errors. * update function comments in cloud_preset.go * updated conditional logic on server-statefulset.yaml to also look for cloud secret name in addition to cloud enabled when setting the cloud stanza * updated getDeepyCopyOfValidBootstrapConfig() to get DeepCopy.... * removing unused test logic Co-authored-by: Kyle Schochenmaier Co-authored-by: Iryna Shustava * Add global.cloud to values.yaml * Map global.cloud.secreeName to environment variables and hcl in command for server container. * Adding cloud preset and validation for it. * add parsing gnm response to struct. * Added functionality and unit tests for SaveSecretsFromBootstrapConfig() * Added functionality and unit tests for GetHelmConfigWithMapSecretNames * Added functionality and unit tests for FetchAgentBootstrapConfig() * Rename cloud_preset_installer to cloud_preset_helper * hooked preset installer helper to install command. have unit test that works like acceptance test. passing. * changing code to work with the certs that get generated from HCP. affects setting -tls-server-name on get-auto-encrypt-client-ca, server-acl-init, and the acl-init init containers for clients. * Adding CLI Output changes. * Moving and consolidating cloud preset files * Moved preset templates to implementations of a Preset interface * removing upgrade tests * change context.TODO() to context.Background() * docstrings * fixing description for preset flag to properly show the list ofvalid presets. * Renaming coud_prset.go to cloud_set.go * refactor out common logic from the local getPreset functions in install and uninstall commands. * upgrade helm and k8s deps related to customize error. * refactor to use hcp-sdk * removing unused vars * updated based on latest specs. * Refactor usage of sdk client so that it properly picks up environment variables and starts oauth flow. * Make proper use of the resourceid to supply the BootstrapParams. Add the ability to pass in an http client to the install command and the CloudPreset so that TLS can be used with httptest mock server since tls is enforced for the oauth request in the hcp-sdk. * updated to latest hcp-sdk-go-internal version * include HCP_AUTH_URL and HCP_API_HOST in configuring server-statefulset * Adding comment to server-statefulset and commenting out test * update comment to correct hcp-go-sdk * update bats test comments for -tls-server-name * get rid of cli lint error * removing manual test * Apply suggestions from code review Co-authored-by: Kyle Schochenmaier * updating new secrets to not have quotes. using os.Unsetenv in tests * adding global.cloud.enabled * adding space in values.yaml * Apply suggestions from code review Co-authored-by: Iryna Shustava Co-authored-by: Kyle Schochenmaier * fixing test in install_test.go to have cleaner simpler logic around expecting errors. * update function comments in cloud_preset.go * updated conditional logic on server-statefulset.yaml to also look for cloud secret name in addition to cloud enabled when setting the cloud stanza * updated getDeepyCopyOfValidBootstrapConfig() to get DeepCopy.... * removing unused test logic * Adding upgrade functionality for cloud preset. * remove manual upgrade test * Embed hashicups demo helm chart and expose demo falg to install it. * fixing rebase conflicts in install.go * rename demo preset to quickstart preset * adding missing quickstart.go file * enable handling dry runof both consul and consul demo app. * modify hashicups deployments so that theyhave the consul.hashicorp.com/connect-inject annotation set to true so that they are automatically opted into the service mesh * making namespace dynamic in demo helm charts. * name for demo helm chart to consul-demo * basic uninstall working * refactor install and uninstall to re-use functions and components. * replace instance of Consul Demo with use of constant. * using --namespace instead of -n on port forwad command. capitalizing Accessing Consul Demo Applidation UI. * removing remnant test * correcting mergeconflict * removing temporary test * tests for install * fix issue bypassing the correct release name for consul-demo * use interface and mocks for helm actions and add tests. * Adding tests for install and uninstall * Add Ability to install an HCP self-managed cluster (#8) * Add global.cloud to values.yaml * Map global.cloud.secreeName to environment variables and hcl in command for server container. * Adding cloud preset and validation for it. * add parsing gnm response to struct. * Added functionality and unit tests for SaveSecretsFromBootstrapConfig() * Added functionality and unit tests for GetHelmConfigWithMapSecretNames * Added functionality and unit tests for FetchAgentBootstrapConfig() * Rename cloud_preset_installer to cloud_preset_helper * hooked preset installer helper to install command. have unit test that works like acceptance test. passing. * changing code to work with the certs that get generated from HCP. affects setting -tls-server-name on get-auto-encrypt-client-ca, server-acl-init, and the acl-init init containers for clients. * Adding CLI Output changes. * Moving and consolidating cloud preset files * Moved preset templates to implementations of a Preset interface * removing upgrade tests * change context.TODO() to context.Background() * docstrings * fixing description for preset flag to properly show the list ofvalid presets. * Renaming coud_prset.go to cloud_set.go * refactor out common logic from the local getPreset functions in install and uninstall commands. * upgrade helm and k8s deps related to customize error. * refactor to use hcp-sdk * removing unused vars * updated based on latest specs. * Refactor usage of sdk client so that it properly picks up environment variables and starts oauth flow. * Make proper use of the resourceid to supply the BootstrapParams. Add the ability to pass in an http client to the install command and the CloudPreset so that TLS can be used with httptest mock server since tls is enforced for the oauth request in the hcp-sdk. * updated to latest hcp-sdk-go-internal version * include HCP_AUTH_URL and HCP_API_HOST in configuring server-statefulset * Adding comment to server-statefulset and commenting out test * update comment to correct hcp-go-sdk * update bats test comments for -tls-server-name * get rid of cli lint error * removing manual test * Apply suggestions from code review Co-authored-by: Kyle Schochenmaier * updating new secrets to not have quotes. using os.Unsetenv in tests * adding global.cloud.enabled * adding space in values.yaml * Apply suggestions from code review Co-authored-by: Iryna Shustava Co-authored-by: Kyle Schochenmaier * fixing test in install_test.go to have cleaner simpler logic around expecting errors. * update function comments in cloud_preset.go * updated conditional logic on server-statefulset.yaml to also look for cloud secret name in addition to cloud enabled when setting the cloud stanza * updated getDeepyCopyOfValidBootstrapConfig() to get DeepCopy.... * removing unused test logic Co-authored-by: Kyle Schochenmaier Co-authored-by: Iryna Shustava * CHANGELOG: formatting and missing links (#1467) * release 0.48.0 (#1473) * release 0.48.0 * update envoy version to 1.23.1 * put main back into dev (#1476) * added notes hashicup helm docs * fixing failing test * Apply suggestions from code review Co-authored-by: Kyle Schochenmaier * Add Ability to install an HCP self-managed cluster (#8) * Add global.cloud to values.yaml * Map global.cloud.secreeName to environment variables and hcl in command for server container. * Adding cloud preset and validation for it. * add parsing gnm response to struct. * Added functionality and unit tests for SaveSecretsFromBootstrapConfig() * Added functionality and unit tests for GetHelmConfigWithMapSecretNames * Added functionality and unit tests for FetchAgentBootstrapConfig() * Rename cloud_preset_installer to cloud_preset_helper * hooked preset installer helper to install command. have unit test that works like acceptance test. passing. * changing code to work with the certs that get generated from HCP. affects setting -tls-server-name on get-auto-encrypt-client-ca, server-acl-init, and the acl-init init containers for clients. * Adding CLI Output changes. * Moving and consolidating cloud preset files * Moved preset templates to implementations of a Preset interface * removing upgrade tests * change context.TODO() to context.Background() * docstrings * fixing description for preset flag to properly show the list ofvalid presets. * Renaming coud_prset.go to cloud_set.go * refactor out common logic from the local getPreset functions in install and uninstall commands. * upgrade helm and k8s deps related to customize error. * refactor to use hcp-sdk * removing unused vars * updated based on latest specs. * Refactor usage of sdk client so that it properly picks up environment variables and starts oauth flow. * Make proper use of the resourceid to supply the BootstrapParams. Add the ability to pass in an http client to the install command and the CloudPreset so that TLS can be used with httptest mock server since tls is enforced for the oauth request in the hcp-sdk. * updated to latest hcp-sdk-go-internal version * include HCP_AUTH_URL and HCP_API_HOST in configuring server-statefulset * Adding comment to server-statefulset and commenting out test * update comment to correct hcp-go-sdk * update bats test comments for -tls-server-name * get rid of cli lint error * removing manual test * Apply suggestions from code review Co-authored-by: Kyle Schochenmaier * updating new secrets to not have quotes. using os.Unsetenv in tests * adding global.cloud.enabled * adding space in values.yaml * Apply suggestions from code review Co-authored-by: Iryna Shustava Co-authored-by: Kyle Schochenmaier * fixing test in install_test.go to have cleaner simpler logic around expecting errors. * update function comments in cloud_preset.go * updated conditional logic on server-statefulset.yaml to also look for cloud secret name in addition to cloud enabled when setting the cloud stanza * updated getDeepyCopyOfValidBootstrapConfig() to get DeepCopy.... * removing unused test logic Co-authored-by: Kyle Schochenmaier Co-authored-by: Iryna Shustava * Add Ability to install an HCP self-managed cluster (#8) * Add global.cloud to values.yaml * Map global.cloud.secreeName to environment variables and hcl in command for server container. * Adding cloud preset and validation for it. * add parsing gnm response to struct. * Added functionality and unit tests for SaveSecretsFromBootstrapConfig() * Added functionality and unit tests for GetHelmConfigWithMapSecretNames * Added functionality and unit tests for FetchAgentBootstrapConfig() * Rename cloud_preset_installer to cloud_preset_helper * hooked preset installer helper to install command. have unit test that works like acceptance test. passing. * changing code to work with the certs that get generated from HCP. affects setting -tls-server-name on get-auto-encrypt-client-ca, server-acl-init, and the acl-init init containers for clients. * Adding CLI Output changes. * Moving and consolidating cloud preset files * Moved preset templates to implementations of a Preset interface * removing upgrade tests * change context.TODO() to context.Background() * docstrings * fixing description for preset flag to properly show the list ofvalid presets. * Renaming coud_prset.go to cloud_set.go * refactor out common logic from the local getPreset functions in install and uninstall commands. * upgrade helm and k8s deps related to customize error. * refactor to use hcp-sdk * removing unused vars * updated based on latest specs. * Refactor usage of sdk client so that it properly picks up environment variables and starts oauth flow. * Make proper use of the resourceid to supply the BootstrapParams. Add the ability to pass in an http client to the install command and the CloudPreset so that TLS can be used with httptest mock server since tls is enforced for the oauth request in the hcp-sdk. * updated to latest hcp-sdk-go-internal version * include HCP_AUTH_URL and HCP_API_HOST in configuring server-statefulset * Adding comment to server-statefulset and commenting out test * update comment to correct hcp-go-sdk * update bats test comments for -tls-server-name * get rid of cli lint error * removing manual test * Apply suggestions from code review Co-authored-by: Kyle Schochenmaier * updating new secrets to not have quotes. using os.Unsetenv in tests * adding global.cloud.enabled * adding space in values.yaml * Apply suggestions from code review Co-authored-by: Iryna Shustava Co-authored-by: Kyle Schochenmaier * fixing test in install_test.go to have cleaner simpler logic around expecting errors. * update function comments in cloud_preset.go * updated conditional logic on server-statefulset.yaml to also look for cloud secret name in addition to cloud enabled when setting the cloud stanza * updated getDeepyCopyOfValidBootstrapConfig() to get DeepCopy.... * removing unused test logic Co-authored-by: Kyle Schochenmaier Co-authored-by: Iryna Shustava * Add global.cloud to values.yaml * Map global.cloud.secreeName to environment variables and hcl in command for server container. * Adding cloud preset and validation for it. * add parsing gnm response to struct. * Added functionality and unit tests for SaveSecretsFromBootstrapConfig() * Added functionality and unit tests for GetHelmConfigWithMapSecretNames * Added functionality and unit tests for FetchAgentBootstrapConfig() * Rename cloud_preset_installer to cloud_preset_helper * hooked preset installer helper to install command. have unit test that works like acceptance test. passing. * changing code to work with the certs that get generated from HCP. affects setting -tls-server-name on get-auto-encrypt-client-ca, server-acl-init, and the acl-init init containers for clients. * Adding CLI Output changes. * Moving and consolidating cloud preset files * Moved preset templates to implementations of a Preset interface * removing upgrade tests * change context.TODO() to context.Background() * docstrings * fixing description for preset flag to properly show the list ofvalid presets. * Renaming coud_prset.go to cloud_set.go * refactor out common logic from the local getPreset functions in install and uninstall commands. * upgrade helm and k8s deps related to customize error. * refactor to use hcp-sdk * removing unused vars * updated based on latest specs. * Refactor usage of sdk client so that it properly picks up environment variables and starts oauth flow. * Make proper use of the resourceid to supply the BootstrapParams. Add the ability to pass in an http client to the install command and the CloudPreset so that TLS can be used with httptest mock server since tls is enforced for the oauth request in the hcp-sdk. * updated to latest hcp-sdk-go-internal version * include HCP_AUTH_URL and HCP_API_HOST in configuring server-statefulset * Adding comment to server-statefulset and commenting out test * update comment to correct hcp-go-sdk * update bats test comments for -tls-server-name * get rid of cli lint error * removing manual test * Apply suggestions from code review Co-authored-by: Kyle Schochenmaier * updating new secrets to not have quotes. using os.Unsetenv in tests * adding global.cloud.enabled * adding space in values.yaml * Apply suggestions from code review Co-authored-by: Iryna Shustava Co-authored-by: Kyle Schochenmaier * fixing test in install_test.go to have cleaner simpler logic around expecting errors. * update function comments in cloud_preset.go * updated conditional logic on server-statefulset.yaml to also look for cloud secret name in addition to cloud enabled when setting the cloud stanza * updated getDeepyCopyOfValidBootstrapConfig() to get DeepCopy.... * removing unused test logic * Adding upgrade functionality for cloud preset. * remove manual upgrade test * Embed hashicups demo helm chart and expose demo falg to install it. * fixing rebase conflicts in install.go * rename demo preset to quickstart preset * adding missing quickstart.go file * enable handling dry runof both consul and consul demo app. * modify hashicups deployments so that theyhave the consul.hashicorp.com/connect-inject annotation set to true so that they are automatically opted into the service mesh * making namespace dynamic in demo helm charts. * name for demo helm chart to consul-demo * basic uninstall working * refactor install and uninstall to re-use functions and components. * replace instance of Consul Demo with use of constant. * using --namespace instead of -n on port forwad command. capitalizing Accessing Consul Demo Applidation UI. * removing remnant test * correcting mergeconflict * removing temporary test * tests for install * fix issue bypassing the correct release name for consul-demo * use interface and mocks for helm actions and add tests. * Adding tests for install and uninstall * Add Ability to install an HCP self-managed cluster (#8) * Add global.cloud to values.yaml * Map global.cloud.secreeName to environment variables and hcl in command for server container. * Adding cloud preset and validation for it. * add parsing gnm response to struct. * Added functionality and unit tests for SaveSecretsFromBootstrapConfig() * Added functionality and unit tests for GetHelmConfigWithMapSecretNames * Added functionality and unit tests for FetchAgentBootstrapConfig() * Rename cloud_preset_installer to cloud_preset_helper * hooked preset installer helper to install command. have unit test that works like acceptance test. passing. * changing code to work with the certs that get generated from HCP. affects setting -tls-server-name on get-auto-encrypt-client-ca, server-acl-init, and the acl-init init containers for clients. * Adding CLI Output changes. * Moving and consolidating cloud preset files * Moved preset templates to implementations of a Preset interface * removing upgrade tests * change context.TODO() to context.Background() * docstrings * fixing description for preset flag to properly show the list ofvalid presets. * Renaming coud_prset.go to cloud_set.go * refactor out common logic from the local getPreset functions in install and uninstall commands. * upgrade helm and k8s deps related to customize error. * refactor to use hcp-sdk * removing unused vars * updated based on latest specs. * Refactor usage of sdk client so that it properly picks up environment variables and starts oauth flow. * Make proper use of the resourceid to supply the BootstrapParams. Add the ability to pass in an http client to the install command and the CloudPreset so that TLS can be used with httptest mock server since tls is enforced for the oauth request in the hcp-sdk. * updated to latest hcp-sdk-go-internal version * include HCP_AUTH_URL and HCP_API_HOST in configuring server-statefulset * Adding comment to server-statefulset and commenting out test * update comment to correct hcp-go-sdk * update bats test comments for -tls-server-name * get rid of cli lint error * removing manual test * Apply suggestions from code review Co-authored-by: Kyle Schochenmaier * updating new secrets to not have quotes. using os.Unsetenv in tests * adding global.cloud.enabled * adding space in values.yaml * Apply suggestions from code review Co-authored-by: Iryna Shustava Co-authored-by: Kyle Schochenmaier * fixing test in install_test.go to have cleaner simpler logic around expecting errors. * update function comments in cloud_preset.go * updated conditional logic on server-statefulset.yaml to also look for cloud secret name in addition to cloud enabled when setting the cloud stanza * updated getDeepyCopyOfValidBootstrapConfig() to get DeepCopy.... * removing unused test logic Co-authored-by: Kyle Schochenmaier Co-authored-by: Iryna Shustava * CHANGELOG: formatting and missing links (#1467) * release 0.48.0 (#1473) * release 0.48.0 * update envoy version to 1.23.1 * put main back into dev (#1476) * added notes hashicup helm docs * CHANGELOG: formatting and missing links (#1467) * release 0.48.0 (#1473) * release 0.48.0 * update envoy version to 1.23.1 * put main back into dev (#1476) * changing InstallHelmReleaseOptions to InstallHelmReleaseOptions * Enable HashiCups to be installed via the consul-k8s CLI (#6) * Add Ability to install an HCP self-managed cluster (#8) * Add global.cloud to values.yaml * Map global.cloud.secreeName to environment variables and hcl in command for server container. * Adding cloud preset and validation for it. * add parsing gnm response to struct. * Added functionality and unit tests for SaveSecretsFromBootstrapConfig() * Added functionality and unit tests for GetHelmConfigWithMapSecretNames * Added functionality and unit tests for FetchAgentBootstrapConfig() * Rename cloud_preset_installer to cloud_preset_helper * hooked preset installer helper to install command. have unit test that works like acceptance test. passing. * changing code to work with the certs that get generated from HCP. affects setting -tls-server-name on get-auto-encrypt-client-ca, server-acl-init, and the acl-init init containers for clients. * Adding CLI Output changes. * Moving and consolidating cloud preset files * Moved preset templates to implementations of a Preset interface * removing upgrade tests * change context.TODO() to context.Background() * docstrings * fixing description for preset flag to properly show the list ofvalid presets. * Renaming coud_prset.go to cloud_set.go * refactor out common logic from the local getPreset functions in install and uninstall commands. * upgrade helm and k8s deps related to customize error. * refactor to use hcp-sdk * removing unused vars * updated based on latest specs. * Refactor usage of sdk client so that it properly picks up environment variables and starts oauth flow. * Make proper use of the resourceid to supply the BootstrapParams. Add the ability to pass in an http client to the install command and the CloudPreset so that TLS can be used with httptest mock server since tls is enforced for the oauth request in the hcp-sdk. * updated to latest hcp-sdk-go-internal version * include HCP_AUTH_URL and HCP_API_HOST in configuring server-statefulset * Adding comment to server-statefulset and commenting out test * update comment to correct hcp-go-sdk * update bats test comments for -tls-server-name * get rid of cli lint error * removing manual test * Apply suggestions from code review Co-authored-by: Kyle Schochenmaier * updating new secrets to not have quotes. using os.Unsetenv in tests * adding global.cloud.enabled * adding space in values.yaml * Apply suggestions from code review Co-authored-by: Iryna Shustava Co-authored-by: Kyle Schochenmaier * fixing test in install_test.go to have cleaner simpler logic around expecting errors. * update function comments in cloud_preset.go * updated conditional logic on server-statefulset.yaml to also look for cloud secret name in addition to cloud enabled when setting the cloud stanza * updated getDeepyCopyOfValidBootstrapConfig() to get DeepCopy.... * removing unused test logic Co-authored-by: Kyle Schochenmaier Co-authored-by: Iryna Shustava * Add global.cloud to values.yaml * Map global.cloud.secreeName to environment variables and hcl in command for server container. * Adding cloud preset and validation for it. * add parsing gnm response to struct. * Added functionality and unit tests for SaveSecretsFromBootstrapConfig() * Added functionality and unit tests for GetHelmConfigWithMapSecretNames * Added functionality and unit tests for FetchAgentBootstrapConfig() * Rename cloud_preset_installer to cloud_preset_helper * hooked preset installer helper to install command. have unit test that works like acceptance test. passing. * changing code to work with the certs that get generated from HCP. affects setting -tls-server-name on get-auto-encrypt-client-ca, server-acl-init, and the acl-init init containers for clients. * Adding CLI Output changes. * Moving and consolidating cloud preset files * Moved preset templates to implementations of a Preset interface * removing upgrade tests * change context.TODO() to context.Background() * docstrings * fixing description for preset flag to properly show the list ofvalid presets. * Renaming coud_prset.go to cloud_set.go * refactor out common logic from the local getPreset functions in install and uninstall commands. * upgrade helm and k8s deps related to customize error. * refactor to use hcp-sdk * removing unused vars * updated based on latest specs. * Refactor usage of sdk client so that it properly picks up environment variables and starts oauth flow. * Make proper use of the resourceid to supply the BootstrapParams. Add the ability to pass in an http client to the install command and the CloudPreset so that TLS can be used with httptest mock server since tls is enforced for the oauth request in the hcp-sdk. * updated to latest hcp-sdk-go-internal version * include HCP_AUTH_URL and HCP_API_HOST in configuring server-statefulset * Adding comment to server-statefulset and commenting out test * update comment to correct hcp-go-sdk * update bats test comments for -tls-server-name * get rid of cli lint error * removing manual test * Apply suggestions from code review Co-authored-by: Kyle Schochenmaier * updating new secrets to not have quotes. using os.Unsetenv in tests * adding global.cloud.enabled * adding space in values.yaml * Apply suggestions from code review Co-authored-by: Iryna Shustava Co-authored-by: Kyle Schochenmaier * fixing test in install_test.go to have cleaner simpler logic around expecting errors. * update function comments in cloud_preset.go * updated conditional logic on server-statefulset.yaml to also look for cloud secret name in addition to cloud enabled when setting the cloud stanza * updated getDeepyCopyOfValidBootstrapConfig() to get DeepCopy.... * removing unused test logic * Adding upgrade functionality for cloud preset. * remove manual upgrade test * Embed hashicups demo helm chart and expose demo falg to install it. * fixing rebase conflicts in install.go * rename demo preset to quickstart preset * adding missing quickstart.go file * enable handling dry runof both consul and consul demo app. * modify hashicups deployments so that theyhave the consul.hashicorp.com/connect-inject annotation set to true so that they are automatically opted into the service mesh * making namespace dynamic in demo helm charts. * name for demo helm chart to consul-demo * basic uninstall working * refactor install and uninstall to re-use functions and components. * replace instance of Consul Demo with use of constant. * using --namespace instead of -n on port forwad command. capitalizing Accessing Consul Demo Applidation UI. * removing remnant test * correcting mergeconflict * removing temporary test * tests for install * fix issue bypassing the correct release name for consul-demo * use interface and mocks for helm actions and add tests. * Adding tests for install and uninstall * Add Ability to install an HCP self-managed cluster (#8) * Add global.cloud to values.yaml * Map global.cloud.secreeName to environment variables and hcl in command for server container. * Adding cloud preset and validation for it. * add parsing gnm response to struct. * Added functionality and unit tests for SaveSecretsFromBootstrapConfig() * Added functionality and unit tests for GetHelmConfigWithMapSecretNames * Added functionality and unit tests for FetchAgentBootstrapConfig() * Rename cloud_preset_installer to cloud_preset_helper * hooked preset installer helper to install command. have unit test that works like acceptance test. passing. * changing code to work with the certs that get generated from HCP. affects setting -tls-server-name on get-auto-encrypt-client-ca, server-acl-init, and the acl-init init containers for clients. * Adding CLI Output changes. * Moving and consolidating cloud preset files * Moved preset templates to implementations of a Preset interface * removing upgrade tests * change context.TODO() to context.Background() * docstrings * fixing description for preset flag to properly show the list ofvalid presets. * Renaming coud_prset.go to cloud_set.go * refactor out common logic from the local getPreset functions in install and uninstall commands. * upgrade helm and k8s deps related to customize error. * refactor to use hcp-sdk * removing unused vars * updated based on latest specs. * Refactor usage of sdk client so that it properly picks up environment variables and starts oauth flow. * Make proper use of the resourceid to supply the BootstrapParams. Add the ability to pass in an http client to the install command and the CloudPreset so that TLS can be used with httptest mock server since tls is enforced for the oauth request in the hcp-sdk. * updated to latest hcp-sdk-go-internal version * include HCP_AUTH_URL and HCP_API_HOST in configuring server-statefulset * Adding comment to server-statefulset and commenting out test * update comment to correct hcp-go-sdk * update bats test comments for -tls-server-name * get rid of cli lint error * removing manual test * Apply suggestions from code review Co-authored-by: Kyle Schochenmaier * updating new secrets to not have quotes. using os.Unsetenv in tests * adding global.cloud.enabled * adding space in values.yaml * Apply suggestions from code review Co-authored-by: Iryna Shustava Co-authored-by: Kyle Schochenmaier * fixing test in install_test.go to have cleaner simpler logic around expecting errors. * update function comments in cloud_preset.go * updated conditional logic on server-statefulset.yaml to also look for cloud secret name in addition to cloud enabled when setting the cloud stanza * updated getDeepyCopyOfValidBootstrapConfig() to get DeepCopy.... * removing unused test logic Co-authored-by: Kyle Schochenmaier Co-authored-by: Iryna Shustava * CHANGELOG: formatting and missing links (#1467) * release 0.48.0 (#1473) * release 0.48.0 * update envoy version to 1.23.1 * put main back into dev (#1476) * added notes hashicup helm docs * CHANGELOG: formatting and missing links (#1467) * release 0.48.0 (#1473) * release 0.48.0 * update envoy version to 1.23.1 * put main back into dev (#1476) Co-authored-by: Kyle Schochenmaier Co-authored-by: Iryna Shustava Co-authored-by: David Yu * fixing rebase issue Co-authored-by: Kyle Schochenmaier Co-authored-by: Iryna Shustava Co-authored-by: David Yu Co-authored-by: Thomas Eckert --- cli/cmd/install/install.go | 150 ++++------ cli/cmd/install/install_test.go | 430 +++++++++++++++++++--------- cli/cmd/status/status_test.go | 16 +- cli/cmd/uninstall/uninstall.go | 41 +-- cli/cmd/uninstall/uninstall_test.go | 232 +++++++++++++-- cli/cmd/upgrade/upgrade.go | 192 +++++++------ cli/cmd/upgrade/upgrade_test.go | 350 +++++++++++++++++++++- cli/helm/action.go | 30 +- cli/helm/chart.go | 4 +- cli/helm/install.go | 140 +++++++++ cli/helm/install_test.go | 82 ++++++ cli/helm/mock.go | 122 +++++++- cli/helm/upgrade.go | 149 ++++++++++ cli/helm/upgrade_test.go | 117 ++++++++ 14 files changed, 1663 insertions(+), 392 deletions(-) create mode 100644 cli/helm/install.go create mode 100644 cli/helm/install_test.go create mode 100644 cli/helm/upgrade.go create mode 100644 cli/helm/upgrade_test.go diff --git a/cli/cmd/install/install.go b/cli/cmd/install/install.go index ab2787f800..20b9f942ad 100644 --- a/cli/cmd/install/install.go +++ b/cli/cmd/install/install.go @@ -1,7 +1,6 @@ package install import ( - "embed" "errors" "fmt" "net/http" @@ -203,6 +202,12 @@ func (c *Command) init() { Default: "", Usage: "Set the Kubernetes context to use.", }) + f.StringVar(&flag.StringVar{ + Name: flagHCPResourceID, + Target: &c.flagHCPResourceID, + Default: "", + Usage: "Set the HCP resource_id when using the 'cloud' preset.", + }) c.help = c.set.Help() } @@ -346,14 +351,6 @@ func (c *Command) Run(args []string) int { release.Configuration = values - // If an enterprise license secret was provided, check that the secret exists and that the enterprise Consul image is set. - if helmVals.Global.EnterpriseLicense.SecretName != "" { - if err := c.checkValidEnterprise(rel.Configuration.Global.EnterpriseLicense.SecretName); err != nil { - c.UI.Output(err.Error(), terminal.WithErrorStyle()) - return 1 - } - c.UI.Output("Valid enterprise Consul secret found.", terminal.WithSuccessStyle()) - } err = c.installConsul(valuesYaml, vals, settings, uiLogger) if err != nil { c.UI.Output(err.Error(), terminal.WithErrorStyle()) @@ -361,7 +358,28 @@ func (c *Command) Run(args []string) int { } if c.flagDemo { - err = c.installDemoApp(settings, uiLogger) + timeout, err := time.ParseDuration(c.flagTimeout) + if err != nil { + c.UI.Output(err.Error(), terminal.WithErrorStyle()) + return 1 + } + options := &helm.InstallOptions{ + ReleaseName: common.ConsulDemoAppReleaseName, + ReleaseType: common.ReleaseTypeConsulDemo, + Namespace: c.flagNamespace, + Values: make(map[string]interface{}), + Settings: settings, + EmbeddedChart: consulChart.DemoHelmChart, + ChartDirName: "demo", + UILogger: uiLogger, + DryRun: c.flagDryRun, + AutoApprove: c.flagAutoApprove, + Wait: c.flagWait, + Timeout: timeout, + UI: c.UI, + HelmActionsRunner: c.helmActionsRunner, + } + err = helm.InstallDemoApp(options) if err != nil { c.UI.Output(err.Error(), terminal.WithErrorStyle()) return 1 @@ -375,6 +393,7 @@ func (c *Command) Run(args []string) int { return 0 } + func (c *Command) installConsul(valuesYaml []byte, vals map[string]interface{}, settings *helmCLI.EnvSettings, uiLogger action.DebugLog) error { // Print out the installation summary. c.UI.Output("Consul Installation Summary", terminal.WithHeaderStyle()) @@ -392,105 +411,32 @@ func (c *Command) installConsul(valuesYaml []byte, vals map[string]interface{}, // aren't double prefixed with "consul-consul-...". vals = common.MergeMaps(config.ConvertToMap(config.GlobalNameConsul), vals) - err := c.installHelmRelease(common.DefaultReleaseName, vals, - common.ReleaseTypeConsul, settings, &consulChart.ConsulHelmChart, - common.TopLevelChartDirName, uiLogger) + timeout, err := time.ParseDuration(c.flagTimeout) if err != nil { return err } - - c.UI.Output("Consul installed in namespace %q.", c.flagNamespace, terminal.WithSuccessStyle()) - return nil -} - -// installDemoApp will perform the following actions -// - Print out the installation summary. -// - Setup action configuration for Helm Go SDK function calls. -// - Setup the installation action. -// - Load the Helm chart. -// - Run the install. -func (c *Command) installDemoApp(settings *helmCLI.EnvSettings, uiLogger action.DebugLog) error { - const consulDemoChartPath = "demo" - c.UI.Output(fmt.Sprintf("%s Installation Summary", - cases.Title(language.English).String(common.ReleaseTypeConsulDemo)), - terminal.WithHeaderStyle()) - c.UI.Output("Name: %s", common.ConsulDemoAppReleaseName, terminal.WithInfoStyle()) - c.UI.Output("Namespace: %s", c.flagNamespace, terminal.WithInfoStyle()) - c.UI.Output("\n", terminal.WithInfoStyle()) - - err := c.installHelmRelease(common.ConsulDemoAppReleaseName, make(map[string]interface{}), common.ReleaseTypeConsulDemo, - settings, &consulChart.DemoHelmChart, consulDemoChartPath, uiLogger) + installOptions := &helm.InstallOptions{ + ReleaseName: common.DefaultReleaseName, + ReleaseType: common.ReleaseTypeConsul, + Namespace: c.flagNamespace, + Values: vals, + Settings: settings, + EmbeddedChart: consulChart.ConsulHelmChart, + ChartDirName: common.TopLevelChartDirName, + UILogger: uiLogger, + DryRun: c.flagDryRun, + AutoApprove: c.flagAutoApprove, + Wait: c.flagWait, + Timeout: timeout, + UI: c.UI, + HelmActionsRunner: c.helmActionsRunner, + } + + err = helm.InstallHelmRelease(installOptions) if err != nil { return err } - c.UI.Output("Accessing %s UI", cases.Title(language.English).String(common.ReleaseTypeConsulDemo), terminal.WithHeaderStyle()) - port := "8080" - portForwardCmd := fmt.Sprintf("kubectl port-forward deploy/frontend %s:80", port) - if c.flagNamespace != "default" { - portForwardCmd += fmt.Sprintf(" --namespace %s", c.flagNamespace) - } - c.UI.Output(portForwardCmd, terminal.WithInfoStyle()) - c.UI.Output("Browse to http://localhost:%s.", port, terminal.WithInfoStyle()) - return nil -} - -func (c *Command) installHelmRelease(releaseName string, vals map[string]interface{}, - releaseType string, settings *helmCLI.EnvSettings, embeddedChart *embed.FS, - chartDirName string, uiLogger action.DebugLog) error { - if c.flagDryRun { - return nil - } - - if !c.flagAutoApprove { - confirmation, err := c.UI.Input(&terminal.Input{ - Prompt: "Proceed with installation? (y/N)", - Style: terminal.InfoStyle, - Secret: false, - }) - - if err != nil { - return err - } - if common.Abort(confirmation) { - c.UI.Output("Install aborted. Use the command `consul-k8s install -help` to learn how to customize your installation.", - terminal.WithInfoStyle()) - return err - } - } - - c.UI.Output("Installing %s", releaseType, terminal.WithHeaderStyle()) - - // Setup action configuration for Helm Go SDK function calls. - actionConfig := new(action.Configuration) - actionConfig, err := helm.InitActionConfig(actionConfig, c.flagNamespace, settings, uiLogger) - if err != nil { - return err - } - - // Setup the installation action. - install := action.NewInstall(actionConfig) - install.ReleaseName = releaseName - install.Namespace = c.flagNamespace - install.CreateNamespace = true - install.Wait = c.flagWait - install.Timeout = c.timeoutDuration - - // Load the Helm chart. - chart, err := helm.LoadChart(*embeddedChart, chartDirName) - if err != nil { - return err - } - c.UI.Output("Downloaded charts", terminal.WithSuccessStyle()) - - // Run the install. - if c.helmActionsRunner == nil { - c.helmActionsRunner = &helm.ActionRunner{} - } - if _, err = c.helmActionsRunner.Install(install, chart, vals); err != nil { - return err - } - c.UI.Output("%s installed in namespace %q.", releaseType, c.flagNamespace, terminal.WithSuccessStyle()) return nil } diff --git a/cli/cmd/install/install_test.go b/cli/cmd/install/install_test.go index ebd27a9a6b..1d07ce84de 100644 --- a/cli/cmd/install/install_test.go +++ b/cli/cmd/install/install_test.go @@ -1,9 +1,12 @@ package install import ( + "bytes" "context" + "errors" "flag" "fmt" + "io" "os" "testing" @@ -17,6 +20,9 @@ import ( "github.com/posener/complete" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/chart" + helmRelease "helm.sh/helm/v3/pkg/release" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" @@ -24,7 +30,7 @@ import ( ) func TestCheckForPreviousPVCs(t *testing.T) { - c := getInitializedCommand(t) + c := getInitializedCommand(t, nil) c.kubernetes = fake.NewSimpleClientset() createPVC(t, "consul-server-test1", "default", c.kubernetes) @@ -136,7 +142,7 @@ func TestCheckForPreviousSecrets(t *testing.T) { for name, tc := range cases { t.Run(name, func(t *testing.T) { - c := getInitializedCommand(t) + c := getInitializedCommand(t, nil) c.kubernetes = fake.NewSimpleClientset() c.kubernetes.CoreV1().Secrets("consul").Create(context.Background(), tc.secret, metav1.CreateOptions{}) @@ -184,7 +190,7 @@ func TestValidateFlags(t *testing.T) { } for _, testCase := range testCases { - c := getInitializedCommand(t) + c := getInitializedCommand(t, nil) t.Run(testCase.description, func(t *testing.T) { if err := c.validateFlags(testCase.input); err == nil { t.Errorf("Test case should have failed.") @@ -194,17 +200,22 @@ func TestValidateFlags(t *testing.T) { } // getInitializedCommand sets up a command struct for tests. -func getInitializedCommand(t *testing.T) *Command { +func getInitializedCommand(t *testing.T, buf io.Writer) *Command { t.Helper() log := hclog.New(&hclog.LoggerOptions{ Name: "cli", Level: hclog.Info, Output: os.Stdout, }) - + var ui terminal.UI + if buf != nil { + ui = terminal.NewUI(context.Background(), buf) + } else { + ui = terminal.NewBasicUI(context.Background()) + } baseCommand := &common.BaseCommand{ Log: log, - UI: terminal.NewBasicUI(context.TODO()), + UI: ui, } c := &Command{ @@ -215,7 +226,7 @@ func getInitializedCommand(t *testing.T) *Command { } func TestCheckValidEnterprise(t *testing.T) { - c := getInitializedCommand(t) + c := getInitializedCommand(t, nil) c.kubernetes = fake.NewSimpleClientset() secret := &v1.Secret{ ObjectMeta: metav1.ObjectMeta{ @@ -353,7 +364,7 @@ func TestValidateCloudPresets(t *testing.T) { for _, testCase := range testCases { testCase.preProcessingFunc() - c := getInitializedCommand(t) + c := getInitializedCommand(t, nil) t.Run(testCase.description, func(t *testing.T) { err := c.validateFlags(testCase.input) if testCase.expectError { @@ -386,7 +397,7 @@ func TestGetPreset(t *testing.T) { } for _, tc := range testCases { - c := getInitializedCommand(t) + c := getInitializedCommand(t, nil) t.Run(tc.description, func(t *testing.T) { p, err := c.getPreset(tc.presetName) require.NoError(t, err) @@ -403,137 +414,288 @@ func TestGetPreset(t *testing.T) { } func TestInstall(t *testing.T) { - c := getInitializedCommand(t) - c.kubernetes = fake.NewSimpleClientset() - c.helmActionsRunner = &helm.MockActionRunner{} - returnCode := c.Run([]string{ - "--auto-approve", - }) - require.Equal(t, 0, returnCode) -} - -func TestInstall_alreadyInstalled(t *testing.T) { - c := getInitializedCommand(t) - c.kubernetes = fake.NewSimpleClientset() - c.helmActionsRunner = &helm.MockActionRunner{ - CheckForInstallationsReponse: func(options *helm.CheckForInstallationsOptions) (bool, string, string, error) { - return true, "consul", "consul", nil + var k8s kubernetes.Interface + licenseSecretName := "consul-license" + cases := map[string]struct { + input []string + messages []string + helmActionsRunner *helm.MockActionRunner + preProcessingFunc func() + expectedReturnCode int + expectCheckedForConsulInstallations bool + expectCheckedForConsulDemoInstallations bool + expectConsulInstalled bool + expectConsulDemoInstalled bool + }{ + "install with no arguments returns success": { + input: []string{}, + messages: []string{ + "\n==> Checking if Consul can be installed\n ✓ No existing Consul installations found.\n ✓ No existing Consul persistent volume claims found\n ✓ No existing Consul secrets found.\n", + "\n==> Consul Installation Summary\n Name: consul\n Namespace: consul\n \n No overrides provided, using the default Helm values.\n", + "\n==> Installing Consul\n ✓ Downloaded charts.\n ✓ Consul installed in namespace \"consul\".\n", + }, + helmActionsRunner: &helm.MockActionRunner{}, + expectedReturnCode: 0, + expectCheckedForConsulInstallations: true, + expectCheckedForConsulDemoInstallations: false, + expectConsulInstalled: true, + expectConsulDemoInstalled: false, }, - } - returnCode := c.Run([]string{ - "--auto-approve", - }) - require.Equal(t, 1, returnCode) -} - -func TestInstall_existingPVCs(t *testing.T) { - c := getInitializedCommand(t) - c.kubernetes = fake.NewSimpleClientset() - createPVC(t, "consul-server-test1", "default", c.kubernetes) - - c.helmActionsRunner = &helm.MockActionRunner{} - returnCode := c.Run([]string{ - "--auto-approve", - }) - require.Equal(t, 1, returnCode) -} - -func TestInstall_existingSecrets(t *testing.T) { - c := getInitializedCommand(t) - c.kubernetes = fake.NewSimpleClientset() - c.helmActionsRunner = &helm.MockActionRunner{} - secret := &v1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: "consul-secret", - Labels: map[string]string{common.CLILabelKey: common.CLILabelValue}, + "install when consul installation errors returns error": { + input: []string{}, + messages: []string{ + "\n==> Checking if Consul can be installed\n ✓ No existing Consul installations found.\n ✓ No existing Consul persistent volume claims found\n ✓ No existing Consul secrets found.\n", + "\n==> Consul Installation Summary\n Name: consul\n Namespace: consul\n \n No overrides provided, using the default Helm values.\n", + "\n==> Installing Consul\n ✓ Downloaded charts.\n ! Helm returned an error.\n", + }, + helmActionsRunner: &helm.MockActionRunner{ + InstallFunc: func(install *action.Install, chrt *chart.Chart, vals map[string]interface{}) (*helmRelease.Release, error) { + return nil, errors.New("Helm returned an error.") + }, + }, + expectedReturnCode: 1, + expectCheckedForConsulInstallations: true, + expectCheckedForConsulDemoInstallations: false, + expectConsulInstalled: false, + expectConsulDemoInstalled: false, }, - } - createSecret(t, secret, "consul", c.kubernetes) - returnCode := c.Run([]string{ - "--auto-approve", - }) - require.Equal(t, 1, returnCode) -} - -func TestInstall_enterpriseInstallWithSecret(t *testing.T) { - c := getInitializedCommand(t) - c.kubernetes = fake.NewSimpleClientset() - c.helmActionsRunner = &helm.MockActionRunner{} - secretName := "consul-license" - secret := &v1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: secretName, + "install with no arguments when consul installation already exists returns error": { + input: []string{ + "--auto-approve", + }, + messages: []string{ + "\n==> Checking if Consul can be installed\n ! Cannot install Consul. A Consul cluster is already installed in namespace consul with name consul.\n Use the command `consul-k8s uninstall` to uninstall Consul from the cluster.\n", + }, + helmActionsRunner: &helm.MockActionRunner{ + CheckForInstallationsFunc: func(options *helm.CheckForInstallationsOptions) (bool, string, string, error) { + return true, "consul", "consul", nil + }, + }, + expectedReturnCode: 1, + expectCheckedForConsulInstallations: true, + expectCheckedForConsulDemoInstallations: false, + expectConsulInstalled: false, + expectConsulDemoInstalled: false, + }, + "install with no arguments when PVCs exist returns error": { + input: []string{}, + messages: []string{ + "\n==> Checking if Consul can be installed\n ✓ No existing Consul installations found.\n ! found persistent volume claims from previous installations, delete before reinstalling: consul/consul-server-test1\n", + }, + helmActionsRunner: &helm.MockActionRunner{}, + preProcessingFunc: func() { + createPVC(t, "consul-server-test1", "consul", k8s) + }, + expectedReturnCode: 1, + expectCheckedForConsulInstallations: true, + expectCheckedForConsulDemoInstallations: false, + expectConsulInstalled: false, + expectConsulDemoInstalled: false, + }, + "install with no arguments when secrets exist returns error": { + input: []string{ + "--auto-approve", + }, + messages: []string{ + "\n==> Checking if Consul can be installed\n ✓ No existing Consul installations found.\n ✓ No existing Consul persistent volume claims found\n ! Found Consul secrets, possibly from a previous installation.\nDelete existing Consul secrets from Kubernetes:\n\nkubectl delete secret consul-secret --namespace consul\n\n", + }, + helmActionsRunner: &helm.MockActionRunner{}, + preProcessingFunc: func() { + secret := &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "consul-secret", + Labels: map[string]string{common.CLILabelKey: common.CLILabelValue}, + }, + } + createSecret(t, secret, "consul", k8s) + }, + expectedReturnCode: 1, + expectCheckedForConsulInstallations: true, + expectCheckedForConsulDemoInstallations: false, + expectConsulInstalled: false, + expectConsulDemoInstalled: false, + }, + "enterprise install when license secret exists returns success": { + input: []string{ + "--set", fmt.Sprintf("global.enterpriseLicense.secretName=%s", licenseSecretName), + }, + messages: []string{ + "\n==> Checking if Consul can be installed\n ✓ No existing Consul installations found.\n ✓ No existing Consul persistent volume claims found\n ✓ No existing Consul secrets found.\n ✓ Valid enterprise Consul secret found.\n", + "\n==> Consul Installation Summary\n Name: consul\n Namespace: consul\n \n Helm value overrides\n -------------------\n global:\n enterpriseLicense:\n secretName: consul-license\n \n", + "\n==> Installing Consul\n ✓ Downloaded charts.\n ✓ Consul installed in namespace \"consul\".\n", + }, + helmActionsRunner: &helm.MockActionRunner{}, + preProcessingFunc: func() { + secret := &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: licenseSecretName, + }, + } + createSecret(t, secret, "consul", k8s) + }, + expectedReturnCode: 0, + expectCheckedForConsulInstallations: true, + expectCheckedForConsulDemoInstallations: false, + expectConsulInstalled: true, + expectConsulDemoInstalled: false, + }, + "enterprise install when license secret does not exist returns error": { + input: []string{ + "--set", fmt.Sprintf("global.enterpriseLicense.secretName=%s", licenseSecretName), + }, + messages: []string{ + "\n==> Checking if Consul can be installed\n ✓ No existing Consul installations found.\n ✓ No existing Consul persistent volume claims found\n ✓ No existing Consul secrets found.\n ! enterprise license secret \"consul-license\" is not found in the \"consul\" namespace; please make sure that the secret exists in the \"consul\" namespace\n"}, + helmActionsRunner: &helm.MockActionRunner{}, + expectedReturnCode: 1, + expectCheckedForConsulInstallations: true, + expectCheckedForConsulDemoInstallations: false, + expectConsulInstalled: false, + expectConsulDemoInstalled: false, + }, + "install for quickstart preset returns success": { + input: []string{ + "-preset", "quickstart", + }, + messages: []string{ + "\n==> Checking if Consul can be installed\n ✓ No existing Consul installations found.\n ✓ No existing Consul persistent volume claims found\n ✓ No existing Consul secrets found.\n", + "\n==> Consul Installation Summary\n Name: consul\n Namespace: consul\n \n Helm value overrides\n -------------------\n connectInject:\n enabled: true\n metrics:\n defaultEnableMerging: true\n defaultEnabled: true\n enableGatewayMetrics: true\n controller:\n enabled: true\n global:\n metrics:\n enableAgentMetrics: true\n enabled: true\n name: consul\n prometheus:\n enabled: true\n server:\n replicas: 1\n ui:\n enabled: true\n service:\n enabled: true\n \n", + "\n==> Installing Consul\n ✓ Downloaded charts.\n ✓ Consul installed in namespace \"consul\".\n", + }, + helmActionsRunner: &helm.MockActionRunner{}, + expectedReturnCode: 0, + expectCheckedForConsulInstallations: true, + expectCheckedForConsulDemoInstallations: false, + expectConsulInstalled: true, + expectConsulDemoInstalled: false, + }, + "install for secure preset returns success": { + input: []string{ + "-preset", "secure", + }, + messages: []string{ + "\n==> Checking if Consul can be installed\n ✓ No existing Consul installations found.\n ✓ No existing Consul persistent volume claims found\n ✓ No existing Consul secrets found.\n", + "\n==> Consul Installation Summary\n Name: consul\n Namespace: consul\n \n Helm value overrides\n -------------------\n connectInject:\n enabled: true\n controller:\n enabled: true\n global:\n acls:\n manageSystemACLs: true\n gossipEncryption:\n autoGenerate: true\n name: consul\n tls:\n enableAutoEncrypt: true\n enabled: true\n server:\n replicas: 1\n \n", + "\n==> Installing Consul\n ✓ Downloaded charts.\n ✓ Consul installed in namespace \"consul\".\n", + }, + helmActionsRunner: &helm.MockActionRunner{}, + expectedReturnCode: 0, + expectCheckedForConsulInstallations: true, + expectCheckedForConsulDemoInstallations: false, + expectConsulInstalled: true, + expectConsulDemoInstalled: false, + }, + "install with demo flag returns success": { + input: []string{ + "-demo", + }, + messages: []string{ + "\n==> Checking if Consul can be installed\n ✓ No existing Consul installations found.\n ✓ No existing Consul persistent volume claims found\n ✓ No existing Consul secrets found.\n", + "\n==> Checking if Consul Demo Application can be installed\n ✓ No existing Consul demo application installations found.\n", + "\n==> Consul Installation Summary\n Name: consul\n Namespace: consul\n \n No overrides provided, using the default Helm values.\n", + "\n==> Installing Consul\n ✓ Downloaded charts.\n ✓ Consul installed in namespace \"consul\".\n", + "\n==> Consul Demo Application Installation Summary\n Name: consul-demo\n Namespace: consul\n \n \n", + "\n==> Installing Consul demo application\n ✓ Downloaded charts.\n ✓ Consul demo application installed in namespace \"consul\".\n", + "\n==> Accessing Consul Demo Application UI\n kubectl port-forward deploy/frontend 8080:80 --namespace consul\n Browse to http://localhost:8080.\n", + }, + helmActionsRunner: &helm.MockActionRunner{}, + expectedReturnCode: 0, + expectCheckedForConsulInstallations: true, + expectCheckedForConsulDemoInstallations: true, + expectConsulInstalled: true, + expectConsulDemoInstalled: true, + }, + "install with demo flag when consul demo installation errors returns error": { + input: []string{ + "-demo", + }, + messages: []string{ + "\n==> Checking if Consul can be installed\n ✓ No existing Consul installations found.\n ✓ No existing Consul persistent volume claims found\n ✓ No existing Consul secrets found.\n", + "\n==> Checking if Consul Demo Application can be installed\n ✓ No existing Consul demo application installations found.\n", + "\n==> Consul Installation Summary\n Name: consul\n Namespace: consul\n \n No overrides provided, using the default Helm values.\n", + "\n==> Installing Consul\n ✓ Downloaded charts.\n ✓ Consul installed in namespace \"consul\".\n ✓ Consul installed in namespace \"consul\".\n", + "\n==> Consul Demo Application Installation Summary\n Name: consul-demo\n Namespace: consul\n \n \n", + "\n==> Installing Consul demo application\n ✓ Downloaded charts.\n ! Helm returned an error.\n", + }, + helmActionsRunner: &helm.MockActionRunner{ + InstallFunc: func(install *action.Install, chrt *chart.Chart, vals map[string]interface{}) (*helmRelease.Release, error) { + if install.ReleaseName == "consul" { + return &helmRelease.Release{Name: install.ReleaseName}, nil + } + return nil, errors.New("Helm returned an error.") + }, + }, + expectedReturnCode: 1, + expectCheckedForConsulInstallations: true, + expectCheckedForConsulDemoInstallations: true, + expectConsulInstalled: true, + expectConsulDemoInstalled: false, + }, + "install with demo flag when demo is already installed returns error and does not install consul or the demo": { + input: []string{ + "-demo", + }, + messages: []string{ + "\n==> Checking if Consul can be installed\n ✓ No existing Consul installations found.\n ✓ No existing Consul persistent volume claims found\n ✓ No existing Consul secrets found.\n", + "\n==> Checking if Consul Demo Application can be installed\n ! Cannot install Consul demo application. A Consul demo application cluster is already installed in namespace consul-demo with name consul-demo.\n Use the command `consul-k8s uninstall` to uninstall the Consul demo application from the cluster.\n", + }, + helmActionsRunner: &helm.MockActionRunner{ + CheckForInstallationsFunc: func(options *helm.CheckForInstallationsOptions) (bool, string, string, error) { + if options.ReleaseName == "consul" { + return false, "", "", nil + } else { + return true, "consul-demo", "consul-demo", nil + } + }, + }, + expectedReturnCode: 1, + expectCheckedForConsulInstallations: true, + expectCheckedForConsulDemoInstallations: true, + expectConsulInstalled: false, + expectConsulDemoInstalled: false, + }, + "install with --dry-run flag returns success": { + input: []string{ + "--dry-run", + }, + messages: []string{ + "\n==> Performing dry run install. No changes will be made to the cluster.\n", + "\n==> Checking if Consul can be installed\n ✓ No existing Consul installations found.\n ✓ No existing Consul persistent volume claims found\n ✓ No existing Consul secrets found.\n", + "\n==> Consul Installation Summary\n Name: consul\n Namespace: consul\n \n No overrides provided, using the default Helm values.\n ✓ Consul installed in namespace \"consul\".\n Dry run complete. No changes were made to the Kubernetes cluster.\n Installation can proceed with this configuration.\n", + }, + helmActionsRunner: &helm.MockActionRunner{}, + expectedReturnCode: 0, + expectCheckedForConsulInstallations: true, + expectCheckedForConsulDemoInstallations: false, + expectConsulInstalled: false, + expectConsulDemoInstalled: false, }, } - createSecret(t, secret, "consul", c.kubernetes) - returnCode := c.Run([]string{ - "--auto-approve", - "--set", fmt.Sprintf("global.enterpriseLicense.secretName=%s", secretName), - }) - - require.Equal(t, 0, returnCode) -} - -func TestInstall_enterpriseInstallWithoutSecretSecret(t *testing.T) { - c := getInitializedCommand(t) - c.kubernetes = fake.NewSimpleClientset() - c.helmActionsRunner = &helm.MockActionRunner{} - secretName := "consul-license" - returnCode := c.Run([]string{ - "--auto-approve", - "--set", fmt.Sprintf("global.enterpriseLicense.secretName=%s", secretName), - }) - require.Equal(t, 1, returnCode) -} - -func TestInstall_DemoFlag(t *testing.T) { - c := getInitializedCommand(t) - c.kubernetes = fake.NewSimpleClientset() - c.helmActionsRunner = &helm.MockActionRunner{} - returnCode := c.Run([]string{ - "-demo", "--auto-approve", - }) - require.Equal(t, 0, returnCode) -} - -func TestInstall_DemoFlagWhenDemoAlreadyInstalled(t *testing.T) { - c := getInitializedCommand(t) - c.kubernetes = fake.NewSimpleClientset() - c.helmActionsRunner = &helm.MockActionRunner{ - CheckForInstallationsReponse: func(options *helm.CheckForInstallationsOptions) (bool, string, string, error) { - if options.ReleaseName == "consul-demo" { - return true, "consul", "consul", nil - } else { - return true, "", "", nil + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + buf := new(bytes.Buffer) + c := getInitializedCommand(t, buf) + k8s = fake.NewSimpleClientset() + c.kubernetes = k8s + mock := tc.helmActionsRunner + c.helmActionsRunner = mock + if tc.preProcessingFunc != nil { + tc.preProcessingFunc() } - }, + input := append([]string{ + "--auto-approve", + }, tc.input...) + returnCode := c.Run(input) + require.Equal(t, tc.expectedReturnCode, returnCode) + require.Equal(t, tc.expectCheckedForConsulInstallations, mock.CheckedForConsulInstallations) + require.Equal(t, tc.expectCheckedForConsulDemoInstallations, mock.CheckedForConsulDemoInstallations) + require.Equal(t, tc.expectConsulInstalled, mock.ConsulInstalled) + require.Equal(t, tc.expectConsulDemoInstalled, mock.ConsulDemoInstalled) + output := buf.String() + for _, msg := range tc.messages { + require.Contains(t, output, msg) + } + }) } - returnCode := c.Run([]string{ - "-demo", "--auto-approve", - }) - require.Equal(t, 1, returnCode) -} - -func TestInstall_SecurePreset(t *testing.T) { - c := getInitializedCommand(t) - c.kubernetes = fake.NewSimpleClientset() - c.helmActionsRunner = &helm.MockActionRunner{} - returnCode := c.Run([]string{ - "-preset", "quickstart", - "--auto-approve", - }) - require.Equal(t, 0, returnCode) -} - -func TestInstall_QuickstartPreset(t *testing.T) { - c := getInitializedCommand(t) - c.kubernetes = fake.NewSimpleClientset() - c.helmActionsRunner = &helm.MockActionRunner{} - returnCode := c.Run([]string{ - "-preset", "quickstart", - "--auto-approve", - }) - require.Equal(t, 0, returnCode) } func createPVC(t *testing.T, name string, namespace string, k8s kubernetes.Interface) { diff --git a/cli/cmd/status/status_test.go b/cli/cmd/status/status_test.go index b45ffef556..e9d622136c 100644 --- a/cli/cmd/status/status_test.go +++ b/cli/cmd/status/status_test.go @@ -4,11 +4,13 @@ import ( "context" "flag" "fmt" + "io" "os" "testing" "github.com/hashicorp/consul-k8s/cli/common" cmnFlag "github.com/hashicorp/consul-k8s/cli/common/flag" + "github.com/hashicorp/consul-k8s/cli/common/terminal" "github.com/hashicorp/go-hclog" "github.com/posener/complete" "github.com/stretchr/testify/assert" @@ -20,7 +22,7 @@ import ( // TestCheckConsulServers creates a fake stateful set and tests the checkConsulServers function. func TestCheckConsulServers(t *testing.T) { - c := getInitializedCommand(t) + c := getInitializedCommand(t, nil) c.kubernetes = fake.NewSimpleClientset() // First check that no stateful sets causes an error. @@ -100,7 +102,7 @@ func TestCheckConsulServers(t *testing.T) { // TestCheckConsulClients is very similar to TestCheckConsulServers() in structure. func TestCheckConsulClients(t *testing.T) { - c := getInitializedCommand(t) + c := getInitializedCommand(t, nil) c.kubernetes = fake.NewSimpleClientset() // No client daemon set should cause an error. @@ -170,16 +172,22 @@ func TestCheckConsulClients(t *testing.T) { } // getInitializedCommand sets up a command struct for tests. -func getInitializedCommand(t *testing.T) *Command { +func getInitializedCommand(t *testing.T, buf io.Writer) *Command { t.Helper() log := hclog.New(&hclog.LoggerOptions{ Name: "cli", Level: hclog.Info, Output: os.Stdout, }) - + var ui terminal.UI + if buf != nil { + ui = terminal.NewUI(context.Background(), buf) + } else { + ui = terminal.NewBasicUI(context.Background()) + } baseCommand := &common.BaseCommand{ Log: log, + UI: ui, } c := &Command{ diff --git a/cli/cmd/uninstall/uninstall.go b/cli/cmd/uninstall/uninstall.go index f7cd0cda5f..28a7e969b6 100644 --- a/cli/cmd/uninstall/uninstall.go +++ b/cli/cmd/uninstall/uninstall.go @@ -189,7 +189,7 @@ func (c *Command) Run(args []string) int { return 1 } - c.UI.Output(fmt.Sprintf("Existing %s Installation", cases.Title(language.English).String(common.ReleaseTypeConsulDemo)), terminal.WithHeaderStyle()) + c.UI.Output(fmt.Sprintf("Checking if %s can be uninstalled", common.ReleaseTypeConsulDemo), terminal.WithHeaderStyle()) foundConsulDemo, foundDemoReleaseName, foundDemoReleaseNamespace, err := c.findExistingInstallation(&helm.CheckForInstallationsOptions{ Settings: settings, ReleaseName: common.ConsulDemoAppReleaseName, @@ -199,20 +199,10 @@ func (c *Command) Run(args []string) int { if err != nil { c.UI.Output(err.Error(), terminal.WithErrorStyle()) return 1 - } else if foundConsulDemo { - err = c.uninstallHelmRelease(foundDemoReleaseName, foundDemoReleaseNamespace, common.ReleaseTypeConsulDemo, settings, uiLogger, actionConfig) - if err != nil { - c.UI.Output(err.Error(), terminal.WithErrorStyle()) - return 1 - } - } else { - c.UI.Output(fmt.Sprintf("No existing %s installation found", common.ReleaseTypeConsulDemo), terminal.WithInfoStyle()) + } else if !foundConsulDemo { + c.UI.Output(fmt.Sprintf("No existing %s installation found.", common.ReleaseTypeConsulDemo), terminal.WithInfoStyle()) } - c.UI.Output("Existing Consul Installation", terminal.WithHeaderStyle()) - // Search for Consul installation by calling `helm list`. Depends on what's already specified. - // Prompt for approval to uninstall Helm release. - // Actually call out to `helm delete`. found, foundReleaseName, foundReleaseNamespace, err := c.findExistingInstallation(&helm.CheckForInstallationsOptions{ Settings: settings, @@ -224,10 +214,23 @@ func (c *Command) Run(args []string) int { return 1 } - err = c.uninstallHelmRelease(foundReleaseName, foundReleaseNamespace, common.ReleaseTypeConsul, settings, uiLogger, actionConfig) - if err != nil { - c.UI.Output(err.Error(), terminal.WithErrorStyle()) - return 1 + if foundConsulDemo { + err = c.uninstallHelmRelease(foundDemoReleaseName, foundDemoReleaseNamespace, common.ReleaseTypeConsulDemo, settings, uiLogger, actionConfig) + if err != nil { + c.UI.Output(err.Error(), terminal.WithErrorStyle()) + return 1 + } + } else { + c.UI.Output(fmt.Sprintf("No existing %s installation found.", common.ReleaseTypeConsulDemo), terminal.WithInfoStyle()) + } + + c.UI.Output("Checking if Consul can be uninstalled", terminal.WithHeaderStyle()) + if found { + err = c.uninstallHelmRelease(foundReleaseName, foundReleaseNamespace, common.ReleaseTypeConsul, settings, uiLogger, actionConfig) + if err != nil { + c.UI.Output(err.Error(), terminal.WithErrorStyle()) + return 1 + } } // If -auto-approve=true and -wipe-data=false, we should only uninstall the release, and skip deleting resources. @@ -325,6 +328,8 @@ func (c *Command) uninstallHelmRelease(releaseName, namespace, releaseType strin c.UI.Output("Name: %s", releaseName, terminal.WithInfoStyle()) c.UI.Output("Namespace: %s", namespace, terminal.WithInfoStyle()) + // Prompt for approval to uninstall Helm release. + // Actually call out to `helm delete`. if !c.flagAutoApprove { confirmation, err := c.UI.Input(&terminal.Input{ Prompt: "Proceed with uninstall? (y/N)", @@ -401,7 +406,7 @@ func (c *Command) findExistingInstallation(options *helm.CheckForInstallationsOp } else { var notFoundError error if !options.SkipErrorWhenNotFound { - notFoundError = fmt.Errorf("could not find consul installation in namespace %s", c.flagNamespace) + notFoundError = fmt.Errorf("could not find %s installation in cluster", common.ReleaseTypeConsul) } return false, "", "", notFoundError } diff --git a/cli/cmd/uninstall/uninstall_test.go b/cli/cmd/uninstall/uninstall_test.go index 1e1e540785..f4ec79700d 100644 --- a/cli/cmd/uninstall/uninstall_test.go +++ b/cli/cmd/uninstall/uninstall_test.go @@ -1,9 +1,12 @@ package uninstall import ( + "bytes" "context" + "errors" "flag" "fmt" + "io" "os" "testing" @@ -15,15 +18,18 @@ import ( "github.com/posener/complete" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "helm.sh/helm/v3/pkg/action" + helmRelease "helm.sh/helm/v3/pkg/release" batchv1 "k8s.io/api/batch/v1" v1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes/fake" ) func TestDeletePVCs(t *testing.T) { - c := getInitializedCommand(t) + c := getInitializedCommand(t, nil) c.kubernetes = fake.NewSimpleClientset() pvc := &v1.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{ @@ -64,7 +70,7 @@ func TestDeletePVCs(t *testing.T) { } func TestDeleteSecrets(t *testing.T) { - c := getInitializedCommand(t) + c := getInitializedCommand(t, nil) c.kubernetes = fake.NewSimpleClientset() secret := &v1.Secret{ ObjectMeta: metav1.ObjectMeta{ @@ -107,7 +113,7 @@ func TestDeleteSecrets(t *testing.T) { } func TestDeleteServiceAccounts(t *testing.T) { - c := getInitializedCommand(t) + c := getInitializedCommand(t, nil) c.kubernetes = fake.NewSimpleClientset() sa := &v1.ServiceAccount{ ObjectMeta: metav1.ObjectMeta{ @@ -148,7 +154,7 @@ func TestDeleteServiceAccounts(t *testing.T) { } func TestDeleteRoles(t *testing.T) { - c := getInitializedCommand(t) + c := getInitializedCommand(t, nil) c.kubernetes = fake.NewSimpleClientset() role := &rbacv1.Role{ ObjectMeta: metav1.ObjectMeta{ @@ -189,7 +195,7 @@ func TestDeleteRoles(t *testing.T) { } func TestDeleteRoleBindings(t *testing.T) { - c := getInitializedCommand(t) + c := getInitializedCommand(t, nil) c.kubernetes = fake.NewSimpleClientset() rolebinding := &rbacv1.RoleBinding{ ObjectMeta: metav1.ObjectMeta{ @@ -230,7 +236,7 @@ func TestDeleteRoleBindings(t *testing.T) { } func TestDeleteJobs(t *testing.T) { - c := getInitializedCommand(t) + c := getInitializedCommand(t, nil) c.kubernetes = fake.NewSimpleClientset() job := &batchv1.Job{ ObjectMeta: metav1.ObjectMeta{ @@ -271,7 +277,7 @@ func TestDeleteJobs(t *testing.T) { } func TestDeleteClusterRoles(t *testing.T) { - c := getInitializedCommand(t) + c := getInitializedCommand(t, nil) c.kubernetes = fake.NewSimpleClientset() clusterrole := &rbacv1.ClusterRole{ ObjectMeta: metav1.ObjectMeta{ @@ -312,7 +318,7 @@ func TestDeleteClusterRoles(t *testing.T) { } func TestDeleteClusterRoleBindings(t *testing.T) { - c := getInitializedCommand(t) + c := getInitializedCommand(t, nil) c.kubernetes = fake.NewSimpleClientset() clusterrolebinding := &rbacv1.ClusterRoleBinding{ ObjectMeta: metav1.ObjectMeta{ @@ -353,17 +359,22 @@ func TestDeleteClusterRoleBindings(t *testing.T) { } // getInitializedCommand sets up a command struct for tests. -func getInitializedCommand(t *testing.T) *Command { +func getInitializedCommand(t *testing.T, buf io.Writer) *Command { t.Helper() log := hclog.New(&hclog.LoggerOptions{ Name: "cli", Level: hclog.Info, Output: os.Stdout, }) - + var ui terminal.UI + if buf != nil { + ui = terminal.NewUI(context.Background(), buf) + } else { + ui = terminal.NewBasicUI(context.Background()) + } baseCommand := &common.BaseCommand{ Log: log, - UI: terminal.NewBasicUI(context.TODO()), + UI: ui, } c := &Command{ @@ -404,25 +415,186 @@ func TestTaskCreateCommand_AutocompleteArgs(t *testing.T) { } func TestUninstall(t *testing.T) { - c := getInitializedCommand(t) - c.kubernetes = fake.NewSimpleClientset() - c.helmActionsRunner = &helm.MockActionRunner{ - CheckForInstallationsReponse: func(options *helm.CheckForInstallationsOptions) (bool, string, string, error) { - return true, "consul", "consul", nil + var k8s kubernetes.Interface + cases := map[string]struct { + input []string + messages []string + helmActionsRunner *helm.MockActionRunner + preProcessingFunc func() + expectedReturnCode int + expectCheckedForConsulInstallations bool + expectCheckedForConsulDemoInstallations bool + expectConsulUninstalled bool + expectConsulDemoUninstalled bool + }{ + "uninstall when consul installation exists returns success": { + input: []string{}, + messages: []string{ + "\n==> Checking if Consul demo application can be uninstalled\n No existing Consul demo application installation found.\n", + "\n==> Checking if Consul can be uninstalled\n ✓ Existing Consul installation found.\n", + "\n==> Consul Uninstall Summary\n Name: consul\n Namespace: consul\n ✓ Successfully uninstalled Consul Helm release.\n ✓ Skipping deleting PVCs, secrets, and service accounts.\n", + }, + helmActionsRunner: &helm.MockActionRunner{ + CheckForInstallationsFunc: func(options *helm.CheckForInstallationsOptions) (bool, string, string, error) { + if options.ReleaseName == "consul" { + return true, "consul", "consul", nil + } else { + return false, "", "", nil + } + }, + }, + expectedReturnCode: 0, + expectCheckedForConsulInstallations: true, + expectCheckedForConsulDemoInstallations: true, + expectConsulUninstalled: true, + expectConsulDemoUninstalled: false, + }, + "uninstall when consul installation does not exist returns error": { + input: []string{}, + messages: []string{ + "\n==> Checking if Consul demo application can be uninstalled\n No existing Consul demo application installation found.\n ! could not find Consul installation in cluster\n", + }, + helmActionsRunner: &helm.MockActionRunner{ + CheckForInstallationsFunc: func(options *helm.CheckForInstallationsOptions) (bool, string, string, error) { + if options.ReleaseName == "consul" { + return false, "", "", nil + } else { + return false, "", "", nil + } + }, + }, + expectedReturnCode: 1, + expectCheckedForConsulInstallations: true, + expectCheckedForConsulDemoInstallations: true, + expectConsulUninstalled: false, + expectConsulDemoUninstalled: false, + }, + "uninstall with -wipe-data flag processes other rescource and returns success": { + input: []string{ + "-wipe-data", + }, + messages: []string{ + "\n==> Checking if Consul demo application can be uninstalled\n No existing Consul demo application installation found.\n No existing Consul demo application installation found.\n", + "\n==> Checking if Consul can be uninstalled\n ✓ Existing Consul installation found.\n", + "\n==> Consul Uninstall Summary\n Name: consul\n Namespace: consul\n ✓ Successfully uninstalled Consul Helm release.\n", + "\n==> Other Consul Resources\n Deleting data for installation: \n Name: consul\n Namespace consul\n ✓ No PVCs found.\n ✓ No Consul secrets found.\n ✓ No Consul service accounts found.\n ✓ No Consul roles found.\n ✓ No Consul rolebindings found.\n ✓ No Consul jobs found.\n ✓ No Consul cluster roles found.\n ✓ No Consul cluster role bindings found.\n", + }, + helmActionsRunner: &helm.MockActionRunner{ + CheckForInstallationsFunc: func(options *helm.CheckForInstallationsOptions) (bool, string, string, error) { + if options.ReleaseName == "consul" { + return true, "consul", "consul", nil + } else { + return false, "", "", nil + } + }, + }, + expectedReturnCode: 0, + expectCheckedForConsulInstallations: true, + expectCheckedForConsulDemoInstallations: true, + expectConsulUninstalled: true, + expectConsulDemoUninstalled: false, + }, + "uninstall when both consul and consul demo installations exist returns success": { + input: []string{}, + messages: []string{ + "\n==> Checking if Consul demo application can be uninstalled\n ✓ Existing Consul demo application installation found.\n", + "\n==> Consul Demo Application Uninstall Summary\n Name: consul-demo\n Namespace: consul-demo\n ✓ Successfully uninstalled Consul demo application Helm release.\n", + "\n==> Checking if Consul can be uninstalled\n ✓ Existing Consul installation found.\n", + "\n==> Consul Uninstall Summary\n Name: consul\n Namespace: consul\n ✓ Successfully uninstalled Consul Helm release.\n ✓ Skipping deleting PVCs, secrets, and service accounts.\n", + }, + helmActionsRunner: &helm.MockActionRunner{ + CheckForInstallationsFunc: func(options *helm.CheckForInstallationsOptions) (bool, string, string, error) { + if options.ReleaseName == "consul" { + return true, "consul", "consul", nil + } else { + return true, "consul-demo", "consul-demo", nil + } + }, + }, + expectedReturnCode: 0, + expectCheckedForConsulInstallations: true, + expectCheckedForConsulDemoInstallations: true, + expectConsulUninstalled: true, + expectConsulDemoUninstalled: true, + }, + "uninstall when consul uninstall errors returns error": { + input: []string{}, + messages: []string{ + "\n==> Checking if Consul demo application can be uninstalled\n No existing Consul demo application installation found.\n", + "\n==> Checking if Consul can be uninstalled\n ✓ Existing Consul installation found.\n", + "\n==> Consul Uninstall Summary\n Name: consul\n Namespace: consul\n ! Helm returned an error.\n", + }, + helmActionsRunner: &helm.MockActionRunner{ + CheckForInstallationsFunc: func(options *helm.CheckForInstallationsOptions) (bool, string, string, error) { + if options.ReleaseName == "consul" { + return true, "consul", "consul", nil + } else { + return false, "", "", nil + } + }, + UninstallFunc: func(uninstall *action.Uninstall, name string) (*helmRelease.UninstallReleaseResponse, error) { + return nil, errors.New("Helm returned an error.") + }, + }, + expectedReturnCode: 1, + expectCheckedForConsulInstallations: true, + expectCheckedForConsulDemoInstallations: true, + expectConsulUninstalled: false, + expectConsulDemoUninstalled: false, + }, + "uninstall when consul demo is installed consul demo uninstall errors returns error": { + input: []string{}, + messages: []string{ + "\n==> Checking if Consul demo application can be uninstalled\n ✓ Existing Consul demo application installation found.\n", + "\n==> Consul Demo Application Uninstall Summary\n Name: consul-demo\n Namespace: consul-demo\n ! Helm returned an error.\n", + }, + helmActionsRunner: &helm.MockActionRunner{ + CheckForInstallationsFunc: func(options *helm.CheckForInstallationsOptions) (bool, string, string, error) { + if options.ReleaseName == "consul" { + return true, "consul", "consul", nil + } else { + return true, "consul-demo", "consul-demo", nil + } + }, + UninstallFunc: func(uninstall *action.Uninstall, name string) (*helmRelease.UninstallReleaseResponse, error) { + if name == "consul" { + return &helmRelease.UninstallReleaseResponse{}, nil + } else { + return nil, errors.New("Helm returned an error.") + } + }, + }, + expectedReturnCode: 1, + expectCheckedForConsulInstallations: true, + expectCheckedForConsulDemoInstallations: true, + expectConsulUninstalled: false, + expectConsulDemoUninstalled: false, }, } - returnCode := c.Run([]string{ - "--auto-approve", - }) - require.Equal(t, 0, returnCode) -} - -func TestUninstall_noExistingConsul(t *testing.T) { - c := getInitializedCommand(t) - c.kubernetes = fake.NewSimpleClientset() - c.helmActionsRunner = &helm.MockActionRunner{} - returnCode := c.Run([]string{ - "--auto-approve", - }) - require.Equal(t, 1, returnCode) + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + buf := new(bytes.Buffer) + c := getInitializedCommand(t, buf) + k8s = fake.NewSimpleClientset() + c.kubernetes = k8s + mock := tc.helmActionsRunner + c.helmActionsRunner = mock + if tc.preProcessingFunc != nil { + tc.preProcessingFunc() + } + input := append([]string{ + "--auto-approve", + }, tc.input...) + returnCode := c.Run(input) + require.Equal(t, tc.expectedReturnCode, returnCode) + require.Equal(t, tc.expectCheckedForConsulInstallations, mock.CheckedForConsulInstallations) + require.Equal(t, tc.expectCheckedForConsulDemoInstallations, mock.CheckedForConsulDemoInstallations) + require.Equal(t, tc.expectConsulUninstalled, mock.ConsulUninstalled) + require.Equal(t, tc.expectConsulDemoUninstalled, mock.ConsulDemoUninstalled) + output := buf.String() + for _, msg := range tc.messages { + require.Contains(t, output, msg) + } + }) + } } diff --git a/cli/cmd/upgrade/upgrade.go b/cli/cmd/upgrade/upgrade.go index 88ced7f85c..0e7d5ac2f0 100644 --- a/cli/cmd/upgrade/upgrade.go +++ b/cli/cmd/upgrade/upgrade.go @@ -17,7 +17,7 @@ import ( "github.com/hashicorp/consul-k8s/cli/helm" "github.com/hashicorp/consul-k8s/cli/preset" "github.com/posener/complete" - "helm.sh/helm/v3/pkg/action" + helmCLI "helm.sh/helm/v3/pkg/cli" "helm.sh/helm/v3/pkg/cli/values" "helm.sh/helm/v3/pkg/getter" @@ -52,10 +52,15 @@ const ( flagNameContext = "context" flagNameKubeconfig = "kubeconfig" + flagNameDemo = "demo" + defaultDemo = false + flagHCPResourceID = "hcp-resource-id" envHCPClientID = "HCP_CLIENT_ID" envHCPClientSecret = "HCP_CLIENT_SECRET" + + consulDemoChartPath = "demo" ) type Command struct { @@ -81,6 +86,7 @@ type Command struct { flagVerbose bool flagWait bool flagHCPResourceID string + flagDemo bool flagKubeConfig string flagKubeContext string @@ -172,6 +178,13 @@ func (c *Command) init() { Default: "", Usage: "Set the HCP resource_id when using the 'cloud' preset.", }) + f.BoolVar(&flag.BoolVar{ + Name: flagNameDemo, + Target: &c.flagDemo, + Default: defaultDemo, + Usage: fmt.Sprintf("Install %s immediately after installing %s.", + common.ReleaseTypeConsulDemo, common.ReleaseTypeConsul), + }) c.help = c.set.Help() } @@ -233,33 +246,45 @@ func (c *Command) Run(args []string) int { c.UI.Output("Checking if Consul can be upgraded", terminal.WithHeaderStyle()) uiLogger := c.createUILogger() - found, name, namespace, err := c.helmActionsRunner.CheckForInstallations(&helm.CheckForInstallationsOptions{ + found, consulName, consulNamespace, err := c.helmActionsRunner.CheckForInstallations(&helm.CheckForInstallationsOptions{ Settings: settings, ReleaseName: common.DefaultReleaseName, DebugLog: uiLogger, }) - if !found { - c.UI.Output("Cannot upgrade Consul. Existing Consul installation not found. Use the command `consul-k8s install` to install Consul.", terminal.WithErrorStyle()) - return 1 - } - c.UI.Output("Existing Consul installation found to be upgraded.", terminal.WithSuccessStyle()) - c.UI.Output("Name: %s\nNamespace: %s", name, namespace, terminal.WithInfoStyle()) - chart, err := helm.LoadChart(consulChart.ConsulHelmChart, common.TopLevelChartDirName) if err != nil { c.UI.Output(err.Error(), terminal.WithErrorStyle()) return 1 } - c.UI.Output("Loaded charts", terminal.WithSuccessStyle()) - - currentChartValues, err := helm.FetchChartValues(namespace, name, settings, uiLogger) - if err != nil { - c.UI.Output(err.Error(), terminal.WithErrorStyle()) + if !found { + c.UI.Output("Cannot upgrade Consul. Existing Consul installation not found. Use the command `consul-k8s install` to install Consul.", terminal.WithErrorStyle()) return 1 + } else { + c.UI.Output("Existing %s installation found to be upgraded.", common.ReleaseTypeConsul, terminal.WithSuccessStyle()) + c.UI.Output("Name: %s\nNamespace: %s", consulName, consulNamespace, terminal.WithInfoStyle()) + } + + c.UI.Output(fmt.Sprintf("Checking if %s can be upgraded", common.ReleaseTypeConsulDemo), terminal.WithHeaderStyle()) + // Ensure there is not an existing Consul demo installation which would cause a conflict. + foundDemo, demoName, demoNamespace, _ := c.helmActionsRunner.CheckForInstallations(&helm.CheckForInstallationsOptions{ + Settings: settings, + ReleaseName: common.ConsulDemoAppReleaseName, + DebugLog: uiLogger, + }) + if foundDemo { + c.UI.Output("Existing %s installation found to be upgraded.", common.ReleaseTypeConsulDemo, terminal.WithSuccessStyle()) + c.UI.Output("Name: %s\nNamespace: %s", demoName, demoNamespace, terminal.WithInfoStyle()) + } else { + if c.flagDemo { + c.UI.Output("No existing %s installation found, but -demo flag provided. %s will be installed in namespace %s.", + common.ConsulDemoAppReleaseName, common.ConsulDemoAppReleaseName, consulNamespace, terminal.WithInfoStyle()) + } else { + c.UI.Output("No existing %s installation found.", common.ReleaseTypeConsulDemo, terminal.WithInfoStyle()) + } } // Handle preset, value files, and set values logic. - chartValues, err := c.mergeValuesFlagsWithPrecedence(settings, namespace) + chartValues, err := c.mergeValuesFlagsWithPrecedence(settings, consulNamespace) if err != nil { c.UI.Output(err.Error(), terminal.WithErrorStyle()) return 1 @@ -270,66 +295,95 @@ func (c *Command) Run(args []string) int { // aren't double prefixed with "consul-consul-...". chartValues = common.MergeMaps(config.ConvertToMap(config.GlobalNameConsul), chartValues) - // Print out the upgrade summary. - if err = c.printDiff(currentChartValues, chartValues); err != nil { - c.UI.Output("Could not print the different between current and upgraded charts: %v", err, terminal.WithErrorStyle()) + timeout, err := time.ParseDuration(c.flagTimeout) + if err != nil { + c.UI.Output(err.Error(), terminal.WithErrorStyle()) return 1 } - - // Check if the user is OK with the upgrade unless the auto approve or dry run flags are true. - if !c.flagAutoApprove && !c.flagDryRun { - confirmation, err := c.UI.Input(&terminal.Input{ - Prompt: "Proceed with upgrade? (y/N)", - Style: terminal.InfoStyle, - Secret: false, - }) - - if err != nil { - c.UI.Output(err.Error(), terminal.WithErrorStyle()) - return 1 - } - if common.Abort(confirmation) { - c.UI.Output("Upgrade aborted. Use the command `consul-k8s upgrade -help` to learn how to customize your upgrade.", - terminal.WithInfoStyle()) - return 1 - } - } - - if !c.flagDryRun { - c.UI.Output("Upgrading Consul", terminal.WithHeaderStyle()) - } else { - c.UI.Output("Performing Dry Run Upgrade", terminal.WithHeaderStyle()) - } - - // Setup action configuration for Helm Go SDK function calls. - actionConfig := new(action.Configuration) - actionConfig, err = helm.InitActionConfig(actionConfig, namespace, settings, uiLogger) + options := &helm.UpgradeOptions{ + ReleaseName: consulName, + ReleaseType: common.ReleaseTypeConsul, + ReleaseTypeName: common.ReleaseTypeConsul, + Namespace: consulNamespace, + Values: chartValues, + Settings: settings, + EmbeddedChart: consulChart.ConsulHelmChart, + ChartDirName: common.TopLevelChartDirName, + UILogger: uiLogger, + DryRun: c.flagDryRun, + AutoApprove: c.flagAutoApprove, + Wait: c.flagWait, + Timeout: timeout, + UI: c.UI, + HelmActionsRunner: c.helmActionsRunner, + } + + err = helm.UpgradeHelmRelease(options) if err != nil { c.UI.Output(err.Error(), terminal.WithErrorStyle()) return 1 } - // Setup the upgrade action. - upgrade := action.NewUpgrade(actionConfig) - upgrade.Namespace = namespace - upgrade.DryRun = c.flagDryRun - upgrade.Wait = c.flagWait - upgrade.Timeout = c.timeoutDuration - - // Run the upgrade. Note that the dry run config is passed into the upgrade action, so upgrade.Run is called even during a dry run. - _, err = upgrade.Run(common.DefaultReleaseName, chart, chartValues) + timeout, err = time.ParseDuration(c.flagTimeout) if err != nil { c.UI.Output(err.Error(), terminal.WithErrorStyle()) return 1 } + if foundDemo { + options := &helm.UpgradeOptions{ + ReleaseName: demoName, + ReleaseType: common.ReleaseTypeConsulDemo, + ReleaseTypeName: common.ConsulDemoAppReleaseName, + Namespace: demoNamespace, + Values: make(map[string]interface{}), + Settings: settings, + EmbeddedChart: consulChart.DemoHelmChart, + ChartDirName: consulDemoChartPath, + UILogger: uiLogger, + DryRun: c.flagDryRun, + AutoApprove: c.flagAutoApprove, + Wait: c.flagWait, + Timeout: timeout, + UI: c.UI, + HelmActionsRunner: c.helmActionsRunner, + } + + err = helm.UpgradeHelmRelease(options) + if err != nil { + c.UI.Output(err.Error(), terminal.WithErrorStyle()) + return 1 + } + } else if c.flagDemo { + + options := &helm.InstallOptions{ + ReleaseName: common.ConsulDemoAppReleaseName, + ReleaseType: common.ReleaseTypeConsulDemo, + Namespace: settings.Namespace(), + Values: make(map[string]interface{}), + Settings: settings, + EmbeddedChart: consulChart.DemoHelmChart, + ChartDirName: consulDemoChartPath, + UILogger: uiLogger, + DryRun: c.flagDryRun, + AutoApprove: c.flagAutoApprove, + Wait: c.flagWait, + Timeout: timeout, + UI: c.UI, + HelmActionsRunner: c.helmActionsRunner, + } + err = helm.InstallDemoApp(options) + if err != nil { + c.UI.Output(err.Error(), terminal.WithErrorStyle()) + return 1 + } + } + if c.flagDryRun { c.UI.Output("Dry run complete. No changes were made to the Kubernetes cluster.\n"+ "Upgrade can proceed with this configuration.", terminal.WithInfoStyle()) return 0 } - - c.UI.Output("Consul upgraded in namespace %q.", namespace, terminal.WithSuccessStyle()) return 0 } @@ -466,28 +520,6 @@ func (c *Command) createUILogger() func(string, ...interface{}) { } } -// printDiff marshals both maps to YAML and prints the diff between the two. -func (c *Command) printDiff(old, new map[string]interface{}) error { - diff, err := common.Diff(old, new) - if err != nil { - return err - } - - c.UI.Output("\nDifference between user overrides for current and upgraded charts"+ - "\n--------------------------------------------------------------", terminal.WithInfoStyle()) - for _, line := range strings.Split(diff, "\n") { - if strings.HasPrefix(line, "+") { - c.UI.Output(line, terminal.WithDiffAddedStyle()) - } else if strings.HasPrefix(line, "-") { - c.UI.Output(line, terminal.WithDiffRemovedStyle()) - } else { - c.UI.Output(line, terminal.WithDiffUnchangedStyle()) - } - } - - return nil -} - // getPreset is a factory function that, given a string, produces a struct that // implements the Preset interface. If the string is not recognized an error is // returned. diff --git a/cli/cmd/upgrade/upgrade_test.go b/cli/cmd/upgrade/upgrade_test.go index d59a4c1230..7ad1f7fb58 100644 --- a/cli/cmd/upgrade/upgrade_test.go +++ b/cli/cmd/upgrade/upgrade_test.go @@ -1,18 +1,29 @@ package upgrade import ( + "bytes" + "context" + "errors" "flag" "fmt" + "io" "os" "testing" "github.com/hashicorp/consul-k8s/cli/common" cmnFlag "github.com/hashicorp/consul-k8s/cli/common/flag" + "github.com/hashicorp/consul-k8s/cli/common/terminal" + "github.com/hashicorp/consul-k8s/cli/helm" "github.com/hashicorp/consul-k8s/cli/preset" "github.com/hashicorp/go-hclog" "github.com/posener/complete" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/chart" + helmRelease "helm.sh/helm/v3/pkg/release" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/kubernetes/fake" ) // TestValidateFlags tests the validate flags function. @@ -45,7 +56,7 @@ func TestValidateFlags(t *testing.T) { } for _, testCase := range testCases { - c := getInitializedCommand(t) + c := getInitializedCommand(t, nil) t.Run(testCase.description, func(t *testing.T) { if err := c.validateFlags(testCase.input); err == nil { t.Errorf("Test case should have failed.") @@ -55,16 +66,22 @@ func TestValidateFlags(t *testing.T) { } // getInitializedCommand sets up a command struct for tests. -func getInitializedCommand(t *testing.T) *Command { +func getInitializedCommand(t *testing.T, buf io.Writer) *Command { t.Helper() log := hclog.New(&hclog.LoggerOptions{ Name: "cli", Level: hclog.Info, Output: os.Stdout, }) - + var ui terminal.UI + if buf != nil { + ui = terminal.NewUI(context.Background(), buf) + } else { + ui = terminal.NewBasicUI(context.Background()) + } baseCommand := &common.BaseCommand{ Log: log, + UI: ui, } c := &Command{ @@ -124,7 +141,7 @@ func TestGetPreset(t *testing.T) { } for _, tc := range testCases { - c := getInitializedCommand(t) + c := getInitializedCommand(t, nil) t.Run(tc.description, func(t *testing.T) { p, err := c.getPreset(tc.presetName, "consul") require.NoError(t, err) @@ -166,12 +183,12 @@ func TestValidateCloudPresets(t *testing.T) { "Should error on cloud preset when HCP_CLIENT_ID is not provided.", []string{"-preset=cloud", "-hcp-resource-id=foobar"}, func() { - os.Setenv("HCP_CLIENT_ID", "") + os.Unsetenv("HCP_CLIENT_ID") os.Setenv("HCP_CLIENT_SECRET", "bar") }, func() { - os.Setenv("HCP_CLIENT_ID", "") - os.Setenv("HCP_CLIENT_SECRET", "") + os.Unsetenv("HCP_CLIENT_ID") + os.Unsetenv("HCP_CLIENT_SECRET") }, true, }, @@ -180,11 +197,11 @@ func TestValidateCloudPresets(t *testing.T) { []string{"-preset=cloud", "-hcp-resource-id=foobar"}, func() { os.Setenv("HCP_CLIENT_ID", "foo") - os.Setenv("HCP_CLIENT_SECRET", "") + os.Unsetenv("HCP_CLIENT_SECRET") }, func() { - os.Setenv("HCP_CLIENT_ID", "") - os.Setenv("HCP_CLIENT_SECRET", "") + os.Unsetenv("HCP_CLIENT_ID") + os.Unsetenv("HCP_CLIENT_SECRET") }, true, }, @@ -196,8 +213,8 @@ func TestValidateCloudPresets(t *testing.T) { os.Setenv("HCP_CLIENT_SECRET", "bar") }, func() { - os.Setenv("HCP_CLIENT_ID", "") - os.Setenv("HCP_CLIENT_SECRET", "") + os.Unsetenv("HCP_CLIENT_ID") + os.Unsetenv("HCP_CLIENT_SECRET") }, true, }, @@ -209,8 +226,8 @@ func TestValidateCloudPresets(t *testing.T) { os.Setenv("HCP_CLIENT_SECRET", "bar") }, func() { - os.Setenv("HCP_CLIENT_ID", "") - os.Setenv("HCP_CLIENT_SECRET", "") + os.Unsetenv("HCP_CLIENT_ID") + os.Unsetenv("HCP_CLIENT_SECRET") }, true, }, @@ -218,7 +235,7 @@ func TestValidateCloudPresets(t *testing.T) { for _, testCase := range testCases { testCase.preProcessingFunc() - c := getInitializedCommand(t) + c := getInitializedCommand(t, nil) t.Run(testCase.description, func(t *testing.T) { err := c.validateFlags(testCase.input) if testCase.expectError && err == nil { @@ -230,3 +247,306 @@ func TestValidateCloudPresets(t *testing.T) { testCase.postProcessingFunc() } } + +func TestUpgrade(t *testing.T) { + var k8s kubernetes.Interface + cases := map[string]struct { + input []string + messages []string + helmActionsRunner *helm.MockActionRunner + preProcessingFunc func() + expectedReturnCode int + expectCheckedForConsulInstallations bool + expectCheckedForConsulDemoInstallations bool + expectConsulUpgraded bool + expectConsulDemoUpgraded bool + expectConsulDemoInstalled bool + }{ + "upgrade when consul installation exists returns success": { + input: []string{}, + messages: []string{ + "\n==> Checking if Consul can be upgraded\n ✓ Existing Consul installation found to be upgraded.\n Name: consul\n Namespace: consul\n", + "\n==> Checking if Consul demo application can be upgraded\n No existing Consul demo application installation found.\n", + "\n==> Consul Upgrade Summary\n ✓ Downloaded charts.\n \n Difference between user overrides for current and upgraded charts\n --------------------------------------------------------------\n + global:\n + name: consul\n \n", + "\n==> Upgrading Consul\n ✓ Consul upgraded in namespace \"consul\".\n", + }, + helmActionsRunner: &helm.MockActionRunner{ + CheckForInstallationsFunc: func(options *helm.CheckForInstallationsOptions) (bool, string, string, error) { + if options.ReleaseName == "consul" { + return true, "consul", "consul", nil + } else { + return false, "", "", nil + } + }, + }, + expectedReturnCode: 0, + expectCheckedForConsulInstallations: true, + expectCheckedForConsulDemoInstallations: true, + expectConsulUpgraded: true, + expectConsulDemoUpgraded: false, + }, + "upgrade when consul installation does not exists returns error": { + input: []string{}, + messages: []string{ + "\n==> Checking if Consul can be upgraded\n ! Cannot upgrade Consul. Existing Consul installation not found. Use the command `consul-k8s install` to install Consul.\n", + }, + helmActionsRunner: &helm.MockActionRunner{ + CheckForInstallationsFunc: func(options *helm.CheckForInstallationsOptions) (bool, string, string, error) { + if options.ReleaseName == "consul" { + return false, "", "", nil + } else { + return false, "", "", nil + } + }, + }, + expectedReturnCode: 1, + expectCheckedForConsulInstallations: true, + expectCheckedForConsulDemoInstallations: false, + expectConsulUpgraded: false, + expectConsulDemoUpgraded: false, + }, + "upgrade when consul upgrade errors returns error": { + input: []string{}, + messages: []string{ + "\n==> Checking if Consul can be upgraded\n ✓ Existing Consul installation found to be upgraded.\n Name: consul\n Namespace: consul\n", + "\n==> Checking if Consul demo application can be upgraded\n No existing Consul demo application installation found.\n", + "\n==> Consul Upgrade Summary\n ✓ Downloaded charts.\n \n Difference between user overrides for current and upgraded charts\n --------------------------------------------------------------\n + global:\n + name: consul\n \n\n==> Upgrading Consul\n ! Helm returned an error.\n", + }, + helmActionsRunner: &helm.MockActionRunner{ + CheckForInstallationsFunc: func(options *helm.CheckForInstallationsOptions) (bool, string, string, error) { + if options.ReleaseName == "consul" { + return true, "consul", "consul", nil + } else { + return false, "", "", nil + } + }, + UpgradeFunc: func(upgrade *action.Upgrade, name string, chart *chart.Chart, vals map[string]interface{}) (*helmRelease.Release, error) { + return nil, errors.New("Helm returned an error.") + }, + }, + expectedReturnCode: 1, + expectCheckedForConsulInstallations: true, + expectCheckedForConsulDemoInstallations: true, + expectConsulUpgraded: false, + expectConsulDemoUpgraded: false, + }, + "upgrade when demo flag provided but no demo installation exists installs demo and returns success": { + input: []string{ + "-demo", + }, + messages: []string{ + "\n==> Checking if Consul can be upgraded\n ✓ Existing Consul installation found to be upgraded.\n Name: consul\n Namespace: consul\n", + "\n==> Checking if Consul demo application can be upgraded\n No existing consul-demo installation found, but -demo flag provided. consul-demo will be installed in namespace consul.\n", + "\n==> Consul Upgrade Summary\n ✓ Downloaded charts.\n \n Difference between user overrides for current and upgraded charts\n --------------------------------------------------------------\n + global:\n + name: consul\n \n", + "\n==> Upgrading Consul\n ✓ Consul upgraded in namespace \"consul\".\n", + "\n==> Consul Demo Application Installation Summary\n Name: consul-demo\n Namespace: consul\n \n \n", + "\n==> Installing Consul demo application\n ✓ Downloaded charts.\n ✓ Consul demo application installed in namespace \"consul\".\n", + "\n==> Accessing Consul Demo Application UI\n kubectl port-forward deploy/frontend 8080:80 --namespace consul\n Browse to http://localhost:8080.\n", + }, + helmActionsRunner: &helm.MockActionRunner{ + CheckForInstallationsFunc: func(options *helm.CheckForInstallationsOptions) (bool, string, string, error) { + if options.ReleaseName == "consul" { + return true, "consul", "consul", nil + } else { + return false, "", "", nil + } + }, + }, + expectedReturnCode: 0, + expectCheckedForConsulInstallations: true, + expectCheckedForConsulDemoInstallations: true, + expectConsulUpgraded: true, + expectConsulDemoUpgraded: false, + expectConsulDemoInstalled: true, + }, + "upgrade when demo flag provided and demo installation exists upgrades demo and returns success": { + input: []string{ + "-demo", + }, + messages: []string{ + "\n==> Checking if Consul can be upgraded\n ✓ Existing Consul installation found to be upgraded.\n Name: consul\n Namespace: consul\n", + "\n==> Checking if Consul demo application can be upgraded\n ✓ Existing Consul demo application installation found to be upgraded.\n Name: consul-demo\n Namespace: consul-demo\n", + "\n==> Consul Upgrade Summary\n ✓ Downloaded charts.\n \n Difference between user overrides for current and upgraded charts\n --------------------------------------------------------------\n + global:\n + name: consul\n \n", + "\n==> Upgrading Consul\n ✓ Consul upgraded in namespace \"consul\".\n", + "\n==> Consul-Demo Upgrade Summary\n ✓ Downloaded charts.\n \n Difference between user overrides for current and upgraded charts\n --------------------------------------------------------------\n \n", + "\n==> Upgrading consul-demo\n ✓ Consul-Demo upgraded in namespace \"consul-demo\".\n", + }, + helmActionsRunner: &helm.MockActionRunner{ + CheckForInstallationsFunc: func(options *helm.CheckForInstallationsOptions) (bool, string, string, error) { + if options.ReleaseName == "consul" { + return true, "consul", "consul", nil + } else { + return true, "consul-demo", "consul-demo", nil + } + }, + }, + expectedReturnCode: 0, + expectCheckedForConsulInstallations: true, + expectCheckedForConsulDemoInstallations: true, + expectConsulUpgraded: true, + expectConsulDemoUpgraded: true, + expectConsulDemoInstalled: false, + }, + "upgrade when demo flag not provided but demo installation exists upgrades demo and returns success": { + input: []string{}, + messages: []string{ + "\n==> Checking if Consul can be upgraded\n ✓ Existing Consul installation found to be upgraded.\n Name: consul\n Namespace: consul\n", + "\n==> Checking if Consul demo application can be upgraded\n ✓ Existing Consul demo application installation found to be upgraded.\n Name: consul-demo\n Namespace: consul-demo\n", + "\n==> Consul Upgrade Summary\n ✓ Downloaded charts.\n \n Difference between user overrides for current and upgraded charts\n --------------------------------------------------------------\n + global:\n + name: consul\n \n", + "\n==> Upgrading Consul\n ✓ Consul upgraded in namespace \"consul\".\n", + "\n==> Consul-Demo Upgrade Summary\n ✓ Downloaded charts.\n \n Difference between user overrides for current and upgraded charts\n --------------------------------------------------------------\n \n", + "\n==> Upgrading consul-demo\n ✓ Consul-Demo upgraded in namespace \"consul-demo\".\n", + }, + helmActionsRunner: &helm.MockActionRunner{ + CheckForInstallationsFunc: func(options *helm.CheckForInstallationsOptions) (bool, string, string, error) { + if options.ReleaseName == "consul" { + return true, "consul", "consul", nil + } else { + return true, "consul-demo", "consul-demo", nil + } + }, + }, + expectedReturnCode: 0, + expectCheckedForConsulInstallations: true, + expectCheckedForConsulDemoInstallations: true, + expectConsulUpgraded: true, + expectConsulDemoUpgraded: true, + expectConsulDemoInstalled: false, + }, + "upgrade when demo upgrade errors returns error with consul being upgraded but demo not being upgraded": { + input: []string{}, + messages: []string{ + "\n==> Checking if Consul can be upgraded\n ✓ Existing Consul installation found to be upgraded.\n Name: consul\n Namespace: consul\n", + "\n==> Checking if Consul demo application can be upgraded\n ✓ Existing Consul demo application installation found to be upgraded.\n Name: consul-demo\n Namespace: consul-demo\n", + "\n==> Consul Upgrade Summary\n ✓ Downloaded charts.\n \n Difference between user overrides for current and upgraded charts\n --------------------------------------------------------------\n + global:\n + name: consul\n \n", + "\n==> Upgrading Consul\n ✓ Consul upgraded in namespace \"consul\".\n", + "\n==> Consul-Demo Upgrade Summary\n ✓ Downloaded charts.\n \n Difference between user overrides for current and upgraded charts\n --------------------------------------------------------------\n \n", + "\n==> Upgrading consul-demo\n ! Helm returned an error.\n", + }, + helmActionsRunner: &helm.MockActionRunner{ + CheckForInstallationsFunc: func(options *helm.CheckForInstallationsOptions) (bool, string, string, error) { + if options.ReleaseName == "consul" { + return true, "consul", "consul", nil + } else { + return true, "consul-demo", "consul-demo", nil + } + }, + UpgradeFunc: func(upgrade *action.Upgrade, name string, chart *chart.Chart, vals map[string]interface{}) (*helmRelease.Release, error) { + if name == "consul" { + return &helmRelease.Release{}, nil + } else { + return nil, errors.New("Helm returned an error.") + } + }, + }, + expectedReturnCode: 1, + expectCheckedForConsulInstallations: true, + expectCheckedForConsulDemoInstallations: true, + expectConsulUpgraded: true, + expectConsulDemoUpgraded: false, + }, + "upgrade with quickstart preset when consul installation exists returns success": { + input: []string{ + "-preset", "quickstart", + }, + messages: []string{ + "\n==> Checking if Consul can be upgraded\n ✓ Existing Consul installation found to be upgraded.\n Name: consul\n Namespace: consul\n", + "\n==> Checking if Consul demo application can be upgraded\n No existing Consul demo application installation found.\n", + "\n==> Consul Upgrade Summary\n ✓ Downloaded charts.\n \n Difference between user overrides for current and upgraded charts\n --------------------------------------------------------------\n + connectInject:\n + enabled: true\n + metrics:\n + defaultEnableMerging: true\n + defaultEnabled: true\n + enableGatewayMetrics: true\n + controller:\n + enabled: true\n + global:\n + metrics:\n + enableAgentMetrics: true\n + enabled: true\n + name: consul\n + prometheus:\n + enabled: true\n + server:\n + replicas: 1\n + ui:\n + enabled: true\n + service:\n + enabled: true\n \n", + "\n==> Upgrading Consul\n ✓ Consul upgraded in namespace \"consul\".\n", + }, + helmActionsRunner: &helm.MockActionRunner{ + CheckForInstallationsFunc: func(options *helm.CheckForInstallationsOptions) (bool, string, string, error) { + if options.ReleaseName == "consul" { + return true, "consul", "consul", nil + } else { + return false, "", "", nil + } + }, + }, + expectedReturnCode: 0, + expectCheckedForConsulInstallations: true, + expectCheckedForConsulDemoInstallations: true, + expectConsulUpgraded: true, + expectConsulDemoUpgraded: false, + }, + "upgrade with secure preset when consul installation exists returns success": { + input: []string{ + "-preset", "secure", + }, + messages: []string{ + "\n==> Checking if Consul can be upgraded\n ✓ Existing Consul installation found to be upgraded.\n Name: consul\n Namespace: consul\n", + "\n==> Checking if Consul demo application can be upgraded\n No existing Consul demo application installation found.\n", + "\n==> Consul Upgrade Summary\n ✓ Downloaded charts.\n \n Difference between user overrides for current and upgraded charts\n --------------------------------------------------------------\n + connectInject:\n + enabled: true\n + controller:\n + enabled: true\n + global:\n + acls:\n + manageSystemACLs: true\n + gossipEncryption:\n + autoGenerate: true\n + name: consul\n + tls:\n + enableAutoEncrypt: true\n + enabled: true\n + server:\n + replicas: 1\n \n", + "\n==> Upgrading Consul\n ✓ Consul upgraded in namespace \"consul\".\n", + }, + helmActionsRunner: &helm.MockActionRunner{ + CheckForInstallationsFunc: func(options *helm.CheckForInstallationsOptions) (bool, string, string, error) { + if options.ReleaseName == "consul" { + return true, "consul", "consul", nil + } else { + return false, "", "", nil + } + }, + }, + expectedReturnCode: 0, + expectCheckedForConsulInstallations: true, + expectCheckedForConsulDemoInstallations: true, + expectConsulUpgraded: true, + expectConsulDemoUpgraded: false, + }, + "upgrade with --dry-run flag when consul installation exists returns success": { + input: []string{ + "--dry-run", + }, + messages: []string{ + " Performing dry run upgrade. No changes will be made to the cluster.\n", + "\n==> Checking if Consul can be upgraded\n ✓ Existing Consul installation found to be upgraded.\n Name: consul\n Namespace: consul\n", + "\n==> Checking if Consul demo application can be upgraded\n No existing Consul demo application installation found.\n", + "\n==> Consul Upgrade Summary\n ✓ Downloaded charts.\n \n Difference between user overrides for current and upgraded charts\n --------------------------------------------------------------\n + global:\n + name: consul\n \n", + "\n==> Performing Dry Run Upgrade\n Dry run complete. No changes were made to the Kubernetes cluster.\n Upgrade can proceed with this configuration.\n", + }, + helmActionsRunner: &helm.MockActionRunner{ + CheckForInstallationsFunc: func(options *helm.CheckForInstallationsOptions) (bool, string, string, error) { + if options.ReleaseName == "consul" { + return true, "consul", "consul", nil + } else { + return false, "", "", nil + } + }, + }, + expectedReturnCode: 0, + expectCheckedForConsulInstallations: true, + expectCheckedForConsulDemoInstallations: true, + expectConsulUpgraded: false, + expectConsulDemoUpgraded: false, + }, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + buf := new(bytes.Buffer) + c := getInitializedCommand(t, buf) + k8s = fake.NewSimpleClientset() + c.kubernetes = k8s + mock := tc.helmActionsRunner + c.helmActionsRunner = mock + if tc.preProcessingFunc != nil { + tc.preProcessingFunc() + } + input := append([]string{ + "--auto-approve", + }, tc.input...) + returnCode := c.Run(input) + require.Equal(t, tc.expectedReturnCode, returnCode) + require.Equal(t, tc.expectCheckedForConsulInstallations, mock.CheckedForConsulInstallations) + require.Equal(t, tc.expectCheckedForConsulDemoInstallations, mock.CheckedForConsulDemoInstallations) + require.Equal(t, tc.expectConsulUpgraded, mock.ConsulUpgraded) + require.Equal(t, tc.expectConsulDemoUpgraded, mock.ConsulDemoUpgraded) + require.Equal(t, tc.expectConsulDemoInstalled, mock.ConsulDemoInstalled) + output := buf.String() + for _, msg := range tc.messages { + require.Contains(t, output, msg) + } + }) + } +} diff --git a/cli/helm/action.go b/cli/helm/action.go index b15029c4f1..d71014c762 100644 --- a/cli/helm/action.go +++ b/cli/helm/action.go @@ -1,6 +1,7 @@ package helm import ( + "embed" "fmt" "os" @@ -26,12 +27,27 @@ func InitActionConfig(actionConfig *action.Configuration, namespace string, sett return actionConfig, nil } +// HelmActionsRunner is a thin interface over existing Helm actions that normally +// require a Kubernetes cluster. This interface allows us to mock it in tests +// and get better coverage of CLI commands. type HelmActionsRunner interface { + // A thin wrapper around the Helm list function. + CheckForInstallations(options *CheckForInstallationsOptions) (bool, string, string, error) + // A thin wrapper around the Helm status function. + GetStatus(status *action.Status, name string) (*release.Release, error) + // A thin wrapper around the Helm install function. Install(install *action.Install, chrt *chart.Chart, vals map[string]interface{}) (*release.Release, error) + // A thin wrapper around the LoadChart function in consul-k8s CLI that reads the charts withing the embedded fle system. + LoadChart(chart embed.FS, chartDirName string) (*chart.Chart, error) + // A thin wrapper around the Helm uninstall function. Uninstall(uninstall *action.Uninstall, name string) (*release.UninstallReleaseResponse, error) - CheckForInstallations(options *CheckForInstallationsOptions) (bool, string, string, error) + // A thin wrapper around the Helm upgrade function. + Upgrade(upgrade *action.Upgrade, name string, chart *chart.Chart, vals map[string]interface{}) (*release.Release, error) } +// ActionRunner is the implementation of HelmActionsRunner interface that +// truly calls Helm sdk functions and requires a real Kubernetes cluster. It +// is the non-mock implementation of HelmActionsRunner that is used in the CLI. type ActionRunner struct{} func (h *ActionRunner) Uninstall(uninstall *action.Uninstall, name string) (*release.UninstallReleaseResponse, error) { @@ -78,3 +94,15 @@ func (h *ActionRunner) CheckForInstallations(options *CheckForInstallationsOptio } return false, "", "", notFoundError } + +func (h *ActionRunner) GetStatus(status *action.Status, name string) (*release.Release, error) { + return status.Run(name) +} + +func (h *ActionRunner) Upgrade(upgrade *action.Upgrade, name string, chart *chart.Chart, vals map[string]interface{}) (*release.Release, error) { + return upgrade.Run(name, chart, vals) +} + +func (h *ActionRunner) LoadChart(chart embed.FS, chartDirName string) (*chart.Chart, error) { + return LoadChart(chart, chartDirName) +} diff --git a/cli/helm/chart.go b/cli/helm/chart.go index 1a91ee19d5..f679ca591d 100644 --- a/cli/helm/chart.go +++ b/cli/helm/chart.go @@ -29,7 +29,7 @@ func LoadChart(chart embed.FS, chartDirName string) (*chart.Chart, error) { // FetchChartValues will attempt to fetch the values from the currently // installed Helm chart. -func FetchChartValues(namespace, name string, settings *helmCLI.EnvSettings, uiLogger action.DebugLog) (map[string]interface{}, error) { +func FetchChartValues(actionRunner HelmActionsRunner, namespace, name string, settings *helmCLI.EnvSettings, uiLogger action.DebugLog) (map[string]interface{}, error) { cfg := new(action.Configuration) cfg, err := InitActionConfig(cfg, namespace, settings, uiLogger) if err != nil { @@ -37,7 +37,7 @@ func FetchChartValues(namespace, name string, settings *helmCLI.EnvSettings, uiL } status := action.NewStatus(cfg) - release, err := status.Run(name) + release, err := actionRunner.GetStatus(status, name) if err != nil { return nil, err } diff --git a/cli/helm/install.go b/cli/helm/install.go new file mode 100644 index 0000000000..1bb5f3c886 --- /dev/null +++ b/cli/helm/install.go @@ -0,0 +1,140 @@ +package helm + +import ( + "embed" + "fmt" + "time" + + "github.com/hashicorp/consul-k8s/cli/common" + "github.com/hashicorp/consul-k8s/cli/common/terminal" + "golang.org/x/text/cases" + "golang.org/x/text/language" + "helm.sh/helm/v3/pkg/action" + helmCLI "helm.sh/helm/v3/pkg/cli" +) + +// InstallOptions is used when calling InstallHelmRelease. +type InstallOptions struct { + // ReleaseName is the name of the Helm release to be installed. + ReleaseName string + // ReleaseType is the helm upgrade type - consul vs consul-demo. + ReleaseType string + // Namespace is the Kubernetes namespace where the release is to be + // installed. + Namespace string + // Values the Helm chart values in a map form. + Values map[string]interface{} + // Settings is the Helm CLI environment settings. + Settings *helmCLI.EnvSettings + // Embedded chart specifies the Consul or Consul Demo Helm chart that has + // been embedded into the consul-k8s CLI. + EmbeddedChart embed.FS + // ChartDirName is the top level directory name fo the EmbeddedChart. + ChartDirName string + // UILogger is a DebugLog used to return messages from Helm to the UI. + UILogger action.DebugLog + // DryRun specifies whether the install/upgrade should actually modify the + // Kubernetes cluster. + DryRun bool + // AutoApprove will bypass any terminal prompts with an automatic yes. + AutoApprove bool + // Wait specifies whether the Helm install should wait until all pods + // are ready. + Wait bool + // Timeout is the duration that Helm will wait for the command to complete + // before it throws an error. + Timeout time.Duration + // UI is the terminal output representation that is used to prompt the user + // and output messages. + UI terminal.UI + // HelmActionsRunner is a thin interface around Helm actions for install, + // upgrade, and uninstall. + HelmActionsRunner HelmActionsRunner +} + +// InstallDemoApp will perform the following actions +// - Print out the installation summary. +// - Setup action configuration for Helm Go SDK function calls. +// - Setup the installation action. +// - Load the Helm chart. +// - Run the install. +func InstallDemoApp(options *InstallOptions) error { + options.UI.Output(fmt.Sprintf("%s Installation Summary", + cases.Title(language.English).String(common.ReleaseTypeConsulDemo)), + terminal.WithHeaderStyle()) + options.UI.Output("Name: %s", common.ConsulDemoAppReleaseName, terminal.WithInfoStyle()) + options.UI.Output("Namespace: %s", options.Settings.Namespace(), terminal.WithInfoStyle()) + options.UI.Output("\n", terminal.WithInfoStyle()) + + err := InstallHelmRelease(options) + if err != nil { + return err + } + + options.UI.Output("Accessing %s UI", cases.Title(language.English).String(common.ReleaseTypeConsulDemo), terminal.WithHeaderStyle()) + port := "8080" + portForwardCmd := fmt.Sprintf("kubectl port-forward deploy/frontend %s:80", port) + if options.Settings.Namespace() != "default" { + portForwardCmd += fmt.Sprintf(" --namespace %s", options.Settings.Namespace()) + } + options.UI.Output(portForwardCmd, terminal.WithInfoStyle()) + options.UI.Output("Browse to http://localhost:%s.", port, terminal.WithInfoStyle()) + return nil +} + +// InstallHelmRelease handles downloading the embedded helm chart, loading the +// values and runnning the Helm install command. +func InstallHelmRelease(options *InstallOptions) error { + if options.DryRun { + return nil + } + + if !options.AutoApprove { + confirmation, err := options.UI.Input(&terminal.Input{ + Prompt: "Proceed with installation? (y/N)", + Style: terminal.InfoStyle, + Secret: false, + }) + + if err != nil { + return err + } + if common.Abort(confirmation) { + options.UI.Output("Install aborted. Use the command `consul-k8s install -help` to learn how to customize your installation.", + terminal.WithInfoStyle()) + return err + } + } + + options.UI.Output("Installing %s", options.ReleaseType, terminal.WithHeaderStyle()) + + // Setup action configuration for Helm Go SDK function calls. + actionConfig := new(action.Configuration) + actionConfig, err := InitActionConfig(actionConfig, options.Namespace, options.Settings, options.UILogger) + if err != nil { + return err + } + + // Setup the installation action. + install := action.NewInstall(actionConfig) + install.ReleaseName = options.ReleaseName + install.Namespace = options.Namespace + install.CreateNamespace = true + install.Wait = options.Wait + install.Timeout = options.Timeout + + // Load the Helm chart. + chart, err := options.HelmActionsRunner.LoadChart(options.EmbeddedChart, options.ChartDirName) + if err != nil { + return err + } + options.UI.Output("Downloaded charts.", terminal.WithSuccessStyle()) + + // Run the install. + if _, err = options.HelmActionsRunner.Install(install, chart, options.Values); err != nil { + return err + } + + options.UI.Output("%s installed in namespace %q.", options.ReleaseType, options.Namespace, terminal.WithSuccessStyle()) + return nil +} diff --git a/cli/helm/install_test.go b/cli/helm/install_test.go new file mode 100644 index 0000000000..2cd98ca5a8 --- /dev/null +++ b/cli/helm/install_test.go @@ -0,0 +1,82 @@ +package helm + +import ( + "bytes" + "context" + "embed" + "errors" + "testing" + + "github.com/hashicorp/consul-k8s/cli/common" + "github.com/hashicorp/consul-k8s/cli/common/terminal" + "github.com/stretchr/testify/require" + "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/chart" + helmCLI "helm.sh/helm/v3/pkg/cli" + "helm.sh/helm/v3/pkg/release" +) + +func TestInstallDemoApp(t *testing.T) { + cases := map[string]struct { + messages []string + helmActionsRunner *MockActionRunner + expectError bool + }{ + "basic success": { + messages: []string{ + "\n==> Consul Demo Application Installation Summary\n Name: consul-demo\n Namespace: default\n \n \n", + "\n==> Installing Consul\n ✓ Downloaded charts.\n ✓ Consul installed in namespace \"consul-namespace\".\n", + "\n==> Accessing Consul Demo Application UI\n kubectl port-forward deploy/frontend 8080:80 --namespace consul-namespace\n Browse to http://localhost:8080.\n", + }, + helmActionsRunner: &MockActionRunner{}, + }, + "failure because LoadChart returns failure": { + messages: []string{ + "\n==> Consul Demo Application Installation Summary\n Name: consul-demo\n Namespace: default\n \n \n\n==> Installing Consul\n", + }, + helmActionsRunner: &MockActionRunner{ + LoadChartFunc: func(chrt embed.FS, chartDirName string) (*chart.Chart, error) { + return nil, errors.New("sad trombone!") + }, + }, + expectError: true, + }, + "failure because Install returns failure": { + messages: []string{ + "\n==> Consul Demo Application Installation Summary\n Name: consul-demo\n Namespace: default\n \n \n\n==> Installing Consul\n", + }, + helmActionsRunner: &MockActionRunner{ + InstallFunc: func(install *action.Install, chrt *chart.Chart, vals map[string]interface{}) (*release.Release, error) { + return nil, errors.New("sad trombone!") + }, + }, + expectError: true, + }, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + buf := new(bytes.Buffer) + mock := tc.helmActionsRunner + options := &InstallOptions{ + HelmActionsRunner: mock, + UI: terminal.NewUI(context.Background(), buf), + UILogger: func(format string, v ...interface{}) {}, + ReleaseName: "consul-release", + ReleaseType: common.ReleaseTypeConsul, + Namespace: "consul-namespace", + Settings: helmCLI.New(), + AutoApprove: true, + } + err := InstallDemoApp(options) + if tc.expectError { + require.Error(t, err) + } else { + require.NoError(t, err) + } + output := buf.String() + for _, msg := range tc.messages { + require.Contains(t, output, msg) + } + }) + } +} diff --git a/cli/helm/mock.go b/cli/helm/mock.go index cee635d47a..05d3b6edb4 100644 --- a/cli/helm/mock.go +++ b/cli/helm/mock.go @@ -1,26 +1,136 @@ package helm import ( + "embed" + + "github.com/hashicorp/consul-k8s/cli/common" + "helm.sh/helm/v3/pkg/action" "helm.sh/helm/v3/pkg/chart" "helm.sh/helm/v3/pkg/release" ) type MockActionRunner struct { - CheckForInstallationsReponse func(options *CheckForInstallationsOptions) (bool, string, string, error) + CheckForInstallationsFunc func(options *CheckForInstallationsOptions) (bool, string, string, error) + GetStatusFunc func(status *action.Status, name string) (*release.Release, error) + InstallFunc func(install *action.Install, chrt *chart.Chart, vals map[string]interface{}) (*release.Release, error) + LoadChartFunc func(chrt embed.FS, chartDirName string) (*chart.Chart, error) + UninstallFunc func(uninstall *action.Uninstall, name string) (*release.UninstallReleaseResponse, error) + UpgradeFunc func(upgrade *action.Upgrade, name string, chart *chart.Chart, vals map[string]interface{}) (*release.Release, error) + CheckedForConsulInstallations bool + CheckedForConsulDemoInstallations bool + GotStatusConsulRelease bool + GotStatusConsulDemoRelease bool + ConsulInstalled bool + ConsulUninstalled bool + ConsulUpgraded bool + ConsulDemoInstalled bool + ConsulDemoUninstalled bool + ConsulDemoUpgraded bool } func (m *MockActionRunner) Install(install *action.Install, chrt *chart.Chart, vals map[string]interface{}) (*release.Release, error) { - return &release.Release{}, nil + var installFunc func(install *action.Install, chrt *chart.Chart, vals map[string]interface{}) (*release.Release, error) + if m.InstallFunc == nil { + installFunc = func(install *action.Install, chrt *chart.Chart, vals map[string]interface{}) (*release.Release, error) { + return &release.Release{}, nil + } + } else { + installFunc = m.InstallFunc + } + + release, err := installFunc(install, chrt, vals) + if err == nil { + if install.ReleaseName == common.DefaultReleaseName { + m.ConsulInstalled = true + } else if install.ReleaseName == common.ConsulDemoAppReleaseName { + m.ConsulDemoInstalled = true + } + } + return release, err } func (m *MockActionRunner) Uninstall(uninstall *action.Uninstall, name string) (*release.UninstallReleaseResponse, error) { - return &release.UninstallReleaseResponse{}, nil + var uninstallFunc func(uninstall *action.Uninstall, name string) (*release.UninstallReleaseResponse, error) + + if m.UninstallFunc == nil { + uninstallFunc = func(uninstall *action.Uninstall, name string) (*release.UninstallReleaseResponse, error) { + return &release.UninstallReleaseResponse{}, nil + } + } else { + uninstallFunc = m.UninstallFunc + } + + release, err := uninstallFunc(uninstall, name) + if err == nil { + if name == common.DefaultReleaseName { + m.ConsulUninstalled = true + } else if name == common.ConsulDemoAppReleaseName { + m.ConsulDemoUninstalled = true + } + } + return release, err } -func (h *MockActionRunner) CheckForInstallations(options *CheckForInstallationsOptions) (bool, string, string, error) { - if h.CheckForInstallationsReponse == nil { +func (m *MockActionRunner) CheckForInstallations(options *CheckForInstallationsOptions) (bool, string, string, error) { + if options.ReleaseName == common.DefaultReleaseName { + m.CheckedForConsulInstallations = true + } else if options.ReleaseName == common.ConsulDemoAppReleaseName { + m.CheckedForConsulDemoInstallations = true + } + + if m.CheckForInstallationsFunc == nil { return false, "", "", nil } - return h.CheckForInstallationsReponse(options) + return m.CheckForInstallationsFunc(options) +} + +func (m *MockActionRunner) GetStatus(status *action.Status, name string) (*release.Release, error) { + if name == common.DefaultReleaseName { + m.GotStatusConsulRelease = true + } else if name == common.ConsulDemoAppReleaseName { + m.GotStatusConsulDemoRelease = true + } + + if m.GetStatusFunc == nil { + return &release.Release{}, nil + } + return m.GetStatusFunc(status, name) +} + +func (m *MockActionRunner) Upgrade(upgrade *action.Upgrade, name string, chrt *chart.Chart, vals map[string]interface{}) (*release.Release, error) { + var upgradeFunc func(upgrade *action.Upgrade, name string, chrt *chart.Chart, vals map[string]interface{}) (*release.Release, error) + + if m.UpgradeFunc == nil { + upgradeFunc = func(upgrade *action.Upgrade, name string, chrt *chart.Chart, vals map[string]interface{}) (*release.Release, error) { + return &release.Release{}, nil + } + } else { + upgradeFunc = m.UpgradeFunc + } + + release, err := upgradeFunc(upgrade, name, chrt, vals) + if err == nil { + if name == common.DefaultReleaseName { + m.ConsulUpgraded = true + } else if name == common.ConsulDemoAppReleaseName { + m.ConsulDemoUpgraded = true + } + } + return release, err +} + +func (m *MockActionRunner) LoadChart(chrt embed.FS, chartDirName string) (*chart.Chart, error) { + var loadChartFunc func(chrt embed.FS, chartDirName string) (*chart.Chart, error) + + if m.LoadChartFunc == nil { + loadChartFunc = func(chrt embed.FS, chartDirName string) (*chart.Chart, error) { + return &chart.Chart{}, nil + } + } else { + loadChartFunc = m.LoadChartFunc + } + + release, err := loadChartFunc(chrt, chartDirName) + return release, err } diff --git a/cli/helm/upgrade.go b/cli/helm/upgrade.go new file mode 100644 index 0000000000..d2b8523c5f --- /dev/null +++ b/cli/helm/upgrade.go @@ -0,0 +1,149 @@ +package helm + +import ( + "embed" + "strings" + "time" + + "github.com/hashicorp/consul-k8s/cli/common" + "github.com/hashicorp/consul-k8s/cli/common/terminal" + "golang.org/x/text/cases" + "golang.org/x/text/language" + "helm.sh/helm/v3/pkg/action" + helmCLI "helm.sh/helm/v3/pkg/cli" +) + +// UpgradeOptions is used when calling UpgradeHelmRelease. +type UpgradeOptions struct { + // ReleaseName is the name of the installed Helm release to upgrade. + ReleaseName string + // ReleaseType is the helm upgrade type - consul vs consul-demo. + ReleaseType string + // ReleaseTypeName is a user friendly version of ReleaseType. The values + // are consul and consul demo application. + ReleaseTypeName string + // Namespace is the Kubernetes namespace where the release is installed. + Namespace string + // Values the Helm chart values in a map form. + Values map[string]interface{} + // Settings is the Helm CLI environment settings. + Settings *helmCLI.EnvSettings + // Embedded chart specifies the Consul or Consul Demo Helm chart that has + // been embedded into the consul-k8s CLI. + EmbeddedChart embed.FS + // ChartDirName is the top level directory name fo the EmbeddedChart. + ChartDirName string + // UILogger is a DebugLog used to return messages from Helm to the UI. + UILogger action.DebugLog + // DryRun specifies whether the upgrade should actually modify the + // Kubernetes cluster. + DryRun bool + // AutoApprove will bypass any terminal prompts with an automatic yes. + AutoApprove bool + // Wait specifies whether the Helm install should wait until all pods + // are ready. + Wait bool + // Timeout is the duration that Helm will wait for the command to complete + // before it throws an error. + Timeout time.Duration + // UI is the terminal output representation that is used to prompt the user + // and output messages. + UI terminal.UI + // HelmActionsRunner is a thin interface around Helm actions for install, + // upgrade, and uninstall. + HelmActionsRunner HelmActionsRunner +} + +// UpgradeHelmRelease handles downloading the embedded helm chart, loading the +// values, showing the diff between new and installed values, and runnning the +// Helm install command. +func UpgradeHelmRelease(options *UpgradeOptions) error { + options.UI.Output("%s Upgrade Summary", cases.Title(language.English).String(options.ReleaseTypeName), terminal.WithHeaderStyle()) + + chart, err := options.HelmActionsRunner.LoadChart(options.EmbeddedChart, options.ChartDirName) + if err != nil { + return err + } + options.UI.Output("Downloaded charts.", terminal.WithSuccessStyle()) + + currentChartValues, err := FetchChartValues(options.HelmActionsRunner, + options.Namespace, options.ReleaseName, options.Settings, options.UILogger) + if err != nil { + return err + } + + // Print out the upgrade summary. + if err = printDiff(currentChartValues, options.Values, options.UI); err != nil { + options.UI.Output("Could not print the different between current and upgraded charts: %v", err, terminal.WithErrorStyle()) + return err + } + + // Check if the user is OK with the upgrade unless the auto approve or dry run flags are true. + if !options.AutoApprove && !options.DryRun { + confirmation, err := options.UI.Input(&terminal.Input{ + Prompt: "Proceed with upgrade? (y/N)", + Style: terminal.InfoStyle, + Secret: false, + }) + + if err != nil { + return err + } + if common.Abort(confirmation) { + options.UI.Output("Upgrade aborted. Use the command `consul-k8s upgrade -help` to learn how to customize your upgrade.", + terminal.WithInfoStyle()) + return err + } + } + + if !options.DryRun { + options.UI.Output("Upgrading %s", options.ReleaseTypeName, terminal.WithHeaderStyle()) + } else { + options.UI.Output("Performing Dry Run Upgrade", terminal.WithHeaderStyle()) + return nil + } + + // Setup action configuration for Helm Go SDK function calls. + actionConfig := new(action.Configuration) + actionConfig, err = InitActionConfig(actionConfig, options.Namespace, options.Settings, options.UILogger) + if err != nil { + return err + } + + // Setup the upgrade action. + upgrade := action.NewUpgrade(actionConfig) + upgrade.Namespace = options.Namespace + upgrade.DryRun = options.DryRun + upgrade.Wait = options.Wait + upgrade.Timeout = options.Timeout + + // Run the upgrade. Note that the dry run config is passed into the upgrade action, so upgrade.Run is called even during a dry run. + _, err = options.HelmActionsRunner.Upgrade(upgrade, options.ReleaseName, chart, options.Values) + if err != nil { + return err + } + options.UI.Output("%s upgraded in namespace %q.", cases.Title(language.English).String(options.ReleaseTypeName), options.Namespace, terminal.WithSuccessStyle()) + return nil +} + +// printDiff marshals both maps to YAML and prints the diff between the two. +func printDiff(old, new map[string]interface{}, ui terminal.UI) error { + diff, err := common.Diff(old, new) + if err != nil { + return err + } + + ui.Output("\nDifference between user overrides for current and upgraded charts"+ + "\n--------------------------------------------------------------", terminal.WithInfoStyle()) + for _, line := range strings.Split(diff, "\n") { + if strings.HasPrefix(line, "+") { + ui.Output(line, terminal.WithDiffAddedStyle()) + } else if strings.HasPrefix(line, "-") { + ui.Output(line, terminal.WithDiffRemovedStyle()) + } else { + ui.Output(line, terminal.WithDiffUnchangedStyle()) + } + } + + return nil +} diff --git a/cli/helm/upgrade_test.go b/cli/helm/upgrade_test.go new file mode 100644 index 0000000000..9ffb7dc201 --- /dev/null +++ b/cli/helm/upgrade_test.go @@ -0,0 +1,117 @@ +package helm + +import ( + "bytes" + "context" + "embed" + "errors" + "testing" + + "github.com/hashicorp/consul-k8s/cli/common" + "github.com/hashicorp/consul-k8s/cli/common/terminal" + "github.com/stretchr/testify/require" + "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/chart" + helmCLI "helm.sh/helm/v3/pkg/cli" + "helm.sh/helm/v3/pkg/release" +) + +func TestUpgrade(t *testing.T) { + buf := new(bytes.Buffer) + mock := &MockActionRunner{ + CheckForInstallationsFunc: func(options *CheckForInstallationsOptions) (bool, string, string, error) { + if options.ReleaseName == "consul" { + return false, "", "", nil + } else { + return true, "consul-demo", "consul-demo", nil + } + }, + } + + options := &UpgradeOptions{ + HelmActionsRunner: mock, + UI: terminal.NewUI(context.Background(), buf), + UILogger: func(format string, v ...interface{}) {}, + ReleaseName: "consul-release", + ReleaseType: common.ReleaseTypeConsul, + Namespace: "consul-namespace", + Settings: helmCLI.New(), + AutoApprove: true, + } + + expectedMessages := []string{ + "\n==> Upgrade Summary\n ✓ Downloaded charts.\n \n Difference between user overrides for current and upgraded charts\n --------------------------------------------------------------\n \n", + "\n==> Upgrading \n ✓ upgraded in namespace \"consul-namespace\".\n", + } + err := UpgradeHelmRelease(options) + require.NoError(t, err) + output := buf.String() + for _, msg := range expectedMessages { + require.Contains(t, output, msg) + } +} + +func TestUpgradeHelmRelease(t *testing.T) { + cases := map[string]struct { + messages []string + helmActionsRunner *MockActionRunner + expectError bool + }{ + "basic success": { + messages: []string{ + "\n==> Consul Upgrade Summary\n ✓ Downloaded charts.\n \n Difference between user overrides for current and upgraded charts\n --------------------------------------------------------------\n \n", + "\n==> Upgrading Consul\n ✓ Consul upgraded in namespace \"consul-namespace\".\n", + }, + helmActionsRunner: &MockActionRunner{}, + }, + "failure because LoadChart returns failure": { + messages: []string{ + "\n==> Consul Upgrade Summary\n", + }, + helmActionsRunner: &MockActionRunner{ + LoadChartFunc: func(chrt embed.FS, chartDirName string) (*chart.Chart, error) { + return nil, errors.New("sad trombone!") + }, + }, + expectError: true, + }, + "failure because Upgrade returns failure": { + messages: []string{ + "\n==> Consul Upgrade Summary\n", + }, + helmActionsRunner: &MockActionRunner{ + UpgradeFunc: func(upgrade *action.Upgrade, name string, chart *chart.Chart, vals map[string]interface{}) (*release.Release, error) { + return nil, errors.New("sad trombone!") + }, + }, + expectError: true, + }, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + buf := new(bytes.Buffer) + mock := tc.helmActionsRunner + options := &UpgradeOptions{ + HelmActionsRunner: mock, + UI: terminal.NewUI(context.Background(), buf), + UILogger: func(format string, v ...interface{}) {}, + ReleaseName: "consul-release", + ReleaseType: common.ReleaseTypeConsul, + ReleaseTypeName: common.ReleaseTypeConsul, + Namespace: "consul-namespace", + Settings: helmCLI.New(), + AutoApprove: true, + } + err := UpgradeHelmRelease(options) + if tc.expectError { + require.Error(t, err) + } else { + require.NoError(t, err) + } + output := buf.String() + for _, msg := range tc.messages { + require.Contains(t, output, msg) + } + }) + } +}