2023-10-24 09:39:18 -04:00
/ *
Copyright ( c ) Edgeless Systems GmbH
SPDX - License - Identifier : AGPL - 3.0 - only
* /
package cmd
import (
2023-11-20 05:17:16 -05:00
"bytes"
2023-10-24 09:39:18 -04:00
"context"
2023-11-20 05:17:16 -05:00
"errors"
2023-10-24 09:39:18 -04:00
"fmt"
2023-12-04 07:40:24 -05:00
"io"
2023-11-20 05:17:16 -05:00
"path/filepath"
2023-10-30 04:30:35 -04:00
"strings"
2023-10-24 09:39:18 -04:00
"testing"
2023-10-26 09:59:13 -04:00
"time"
2023-10-24 09:39:18 -04:00
2023-12-04 07:40:24 -05:00
"github.com/edgelesssys/constellation/v2/internal/atls"
2023-11-20 05:17:16 -05:00
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/cloud/gcpshared"
"github.com/edgelesssys/constellation/v2/internal/config"
"github.com/edgelesssys/constellation/v2/internal/constants"
2023-12-04 07:40:24 -05:00
"github.com/edgelesssys/constellation/v2/internal/constellation"
2023-12-06 04:01:39 -05:00
"github.com/edgelesssys/constellation/v2/internal/constellation/helm"
2023-12-08 10:27:04 -05:00
"github.com/edgelesssys/constellation/v2/internal/constellation/state"
2023-10-24 09:39:18 -04:00
"github.com/edgelesssys/constellation/v2/internal/file"
2023-11-20 05:17:16 -05:00
"github.com/edgelesssys/constellation/v2/internal/kms/uri"
2023-10-24 09:39:18 -04:00
"github.com/edgelesssys/constellation/v2/internal/logger"
2023-12-15 09:45:52 -05:00
"github.com/edgelesssys/constellation/v2/internal/versions"
2023-10-24 09:39:18 -04:00
"github.com/spf13/afero"
"github.com/spf13/pflag"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
2023-11-03 10:47:03 -04:00
// defaultStateFile returns a valid default state for testing.
2023-11-20 05:17:16 -05:00
func defaultStateFile ( csp cloudprovider . Provider ) * state . State {
stateFile := & state . State {
2023-11-03 10:47:03 -04:00
Version : "v1" ,
Infrastructure : state . Infrastructure {
UID : "123" ,
Name : "test-cluster" ,
ClusterEndpoint : "192.0.2.1" ,
InClusterEndpoint : "192.0.2.1" ,
InitSecret : [ ] byte { 0x41 } ,
APIServerCertSANs : [ ] string {
"127.0.0.1" ,
"www.example.com" ,
} ,
IPCidrNode : "0.0.0.0/24" ,
Azure : & state . Azure {
ResourceGroup : "test-rg" ,
SubscriptionID : "test-sub" ,
NetworkSecurityGroupName : "test-nsg" ,
LoadBalancerName : "test-lb" ,
UserAssignedIdentity : "test-uami" ,
AttestationURL : "test-maaUrl" ,
} ,
GCP : & state . GCP {
ProjectID : "test-project" ,
IPCidrPod : "0.0.0.0/24" ,
} ,
} ,
ClusterValues : state . ClusterValues {
ClusterID : "deadbeef" ,
OwnerID : "deadbeef" ,
MeasurementSalt : [ ] byte { 0x41 } ,
} ,
}
2023-11-20 05:17:16 -05:00
switch csp {
case cloudprovider . GCP :
stateFile . Infrastructure . Azure = nil
case cloudprovider . Azure :
stateFile . Infrastructure . GCP = nil
default :
stateFile . Infrastructure . Azure = nil
stateFile . Infrastructure . GCP = nil
}
return stateFile
2023-11-03 10:47:03 -04:00
}
2023-10-24 09:39:18 -04:00
func TestParseApplyFlags ( t * testing . T ) {
require := require . New ( t )
defaultFlags := func ( ) * pflag . FlagSet {
2023-10-26 09:59:13 -04:00
flags := NewApplyCmd ( ) . Flags ( )
// Register persistent flags
2023-10-24 09:39:18 -04:00
flags . String ( "workspace" , "" , "" )
flags . String ( "tf-log" , "NONE" , "" )
flags . Bool ( "force" , false , "" )
flags . Bool ( "debug" , false , "" )
return flags
}
testCases := map [ string ] struct {
flags * pflag . FlagSet
wantFlags applyFlags
wantErr bool
} {
"default flags" : {
flags : defaultFlags ( ) ,
wantFlags : applyFlags {
2023-11-29 08:55:10 -05:00
helmWaitMode : helm . WaitModeAtomic ,
helmTimeout : 10 * time . Minute ,
2023-10-24 09:39:18 -04:00
} ,
} ,
"skip phases" : {
flags : func ( ) * pflag . FlagSet {
flags := defaultFlags ( )
require . NoError ( flags . Set ( "skip-phases" , fmt . Sprintf ( "%s,%s" , skipHelmPhase , skipK8sPhase ) ) )
return flags
} ( ) ,
wantFlags : applyFlags {
2023-11-29 08:55:10 -05:00
skipPhases : newPhases ( skipHelmPhase , skipK8sPhase ) ,
helmWaitMode : helm . WaitModeAtomic ,
helmTimeout : 10 * time . Minute ,
2023-10-24 09:39:18 -04:00
} ,
} ,
"skip helm wait" : {
flags : func ( ) * pflag . FlagSet {
flags := defaultFlags ( )
require . NoError ( flags . Set ( "skip-helm-wait" , "true" ) )
return flags
} ( ) ,
wantFlags : applyFlags {
2023-11-29 08:55:10 -05:00
helmWaitMode : helm . WaitModeNone ,
helmTimeout : 10 * time . Minute ,
2023-10-24 09:39:18 -04:00
} ,
} ,
}
for name , tc := range testCases {
t . Run ( name , func ( t * testing . T ) {
assert := assert . New ( t )
var flags applyFlags
err := flags . parse ( tc . flags )
if tc . wantErr {
assert . Error ( err )
return
}
assert . NoError ( err )
assert . Equal ( tc . wantFlags , flags )
} )
}
}
func TestBackupHelmCharts ( t * testing . T ) {
testCases := map [ string ] struct {
helmApplier helm . Applier
backupClient * stubKubernetesUpgrader
includesUpgrades bool
wantErr bool
} {
"success, no upgrades" : {
helmApplier : & stubRunner { } ,
backupClient : & stubKubernetesUpgrader { } ,
} ,
"success with upgrades" : {
helmApplier : & stubRunner { } ,
backupClient : & stubKubernetesUpgrader { } ,
includesUpgrades : true ,
} ,
"saving charts fails" : {
helmApplier : & stubRunner {
saveChartsErr : assert . AnError ,
} ,
backupClient : & stubKubernetesUpgrader { } ,
wantErr : true ,
} ,
"backup CRDs fails" : {
helmApplier : & stubRunner { } ,
backupClient : & stubKubernetesUpgrader {
backupCRDsErr : assert . AnError ,
} ,
includesUpgrades : true ,
wantErr : true ,
} ,
"backup CRs fails" : {
helmApplier : & stubRunner { } ,
backupClient : & stubKubernetesUpgrader {
backupCRsErr : assert . AnError ,
} ,
includesUpgrades : true ,
wantErr : true ,
} ,
}
for name , tc := range testCases {
t . Run ( name , func ( t * testing . T ) {
assert := assert . New ( t )
a := applyCmd {
fileHandler : file . NewHandler ( afero . NewMemMapFs ( ) ) ,
2023-12-06 04:01:39 -05:00
applier : & stubConstellApplier {
stubKubernetesUpgrader : tc . backupClient ,
} ,
log : logger . NewTest ( t ) ,
2023-10-24 09:39:18 -04:00
}
2023-12-05 10:23:31 -05:00
err := a . backupHelmCharts ( context . Background ( ) , tc . helmApplier , tc . includesUpgrades , "" )
2023-10-24 09:39:18 -04:00
if tc . wantErr {
assert . Error ( err )
return
}
assert . NoError ( err )
if tc . includesUpgrades {
assert . True ( tc . backupClient . backupCRDsCalled )
assert . True ( tc . backupClient . backupCRsCalled )
}
} )
}
}
2023-10-30 04:30:35 -04:00
func TestSkipPhases ( t * testing . T ) {
require := require . New ( t )
2023-11-20 05:17:16 -05:00
assert := assert . New ( t )
2023-10-30 04:30:35 -04:00
cmd := NewApplyCmd ( )
// register persistent flags manually
cmd . Flags ( ) . String ( "workspace" , "" , "" )
cmd . Flags ( ) . Bool ( "force" , true , "" )
cmd . Flags ( ) . String ( "tf-log" , "NONE" , "" )
cmd . Flags ( ) . Bool ( "debug" , false , "" )
require . NoError ( cmd . Flags ( ) . Set ( "skip-phases" , strings . Join ( allPhases ( ) , "," ) ) )
2023-11-20 05:17:16 -05:00
wantPhases := newPhases ( skipInfrastructurePhase , skipInitPhase , skipAttestationConfigPhase , skipCertSANsPhase , skipHelmPhase , skipK8sPhase , skipImagePhase )
2023-10-30 04:30:35 -04:00
var flags applyFlags
err := flags . parse ( cmd . Flags ( ) )
require . NoError ( err )
2023-11-20 05:17:16 -05:00
assert . Equal ( wantPhases , flags . skipPhases )
phases := newPhases ( skipAttestationConfigPhase , skipCertSANsPhase )
assert . True ( phases . contains ( skipAttestationConfigPhase , skipCertSANsPhase ) )
assert . False ( phases . contains ( skipAttestationConfigPhase , skipInitPhase ) )
assert . False ( phases . contains ( skipInitPhase , skipInfrastructurePhase ) )
}
func TestValidateInputs ( t * testing . T ) {
defaultConfig := func ( csp cloudprovider . Provider ) func ( require * require . Assertions , fh file . Handler ) {
return func ( require * require . Assertions , fh file . Handler ) {
cfg := defaultConfigWithExpectedMeasurements ( t , config . Default ( ) , csp )
if csp == cloudprovider . GCP {
require . NoError ( fh . WriteJSON ( "saKey.json" , & gcpshared . ServiceAccountKey {
Type : "service_account" ,
ProjectID : "project_id" ,
PrivateKeyID : "key_id" ,
PrivateKey : "key" ,
ClientEmail : "client_email" ,
ClientID : "client_id" ,
AuthURI : "auth_uri" ,
TokenURI : "token_uri" ,
AuthProviderX509CertURL : "cert" ,
ClientX509CertURL : "client_cert" ,
} ) )
cfg . Provider . GCP . ServiceAccountKeyPath = "saKey.json"
}
require . NoError ( fh . WriteYAML ( constants . ConfigFilename , cfg ) )
}
}
preInitState := func ( csp cloudprovider . Provider ) func ( require * require . Assertions , fh file . Handler ) {
return func ( require * require . Assertions , fh file . Handler ) {
stateFile := defaultStateFile ( csp )
stateFile . ClusterValues = state . ClusterValues { }
require . NoError ( fh . WriteYAML ( constants . StateFilename , stateFile ) )
}
}
postInitState := func ( csp cloudprovider . Provider ) func ( require * require . Assertions , fh file . Handler ) {
return func ( require * require . Assertions , fh file . Handler ) {
require . NoError ( fh . WriteYAML ( constants . StateFilename , defaultStateFile ( csp ) ) )
}
}
defaultMasterSecret := func ( require * require . Assertions , fh file . Handler ) {
require . NoError ( fh . WriteJSON ( constants . MasterSecretFilename , & uri . MasterSecret { } ) )
}
defaultAdminConfig := func ( require * require . Assertions , fh file . Handler ) {
require . NoError ( fh . Write ( constants . AdminConfFilename , [ ] byte ( "admin config" ) ) )
}
defaultTfState := func ( require * require . Assertions , fh file . Handler ) {
require . NoError ( fh . Write ( filepath . Join ( constants . TerraformWorkingDir , "tfvars" ) , [ ] byte ( "tf state" ) ) )
}
testCases := map [ string ] struct {
createConfig func ( require * require . Assertions , fh file . Handler )
createState func ( require * require . Assertions , fh file . Handler )
createMasterSecret func ( require * require . Assertions , fh file . Handler )
createAdminConfig func ( require * require . Assertions , fh file . Handler )
createTfState func ( require * require . Assertions , fh file . Handler )
stdin string
flags applyFlags
wantPhases skipPhases
2023-12-15 09:45:52 -05:00
assert func ( require * require . Assertions , assert * assert . Assertions , conf * config . Config , stateFile * state . State )
2023-11-20 05:17:16 -05:00
wantErr bool
} {
"[upgrade] gcp: all files exist" : {
createConfig : defaultConfig ( cloudprovider . GCP ) ,
createState : postInitState ( cloudprovider . GCP ) ,
createMasterSecret : defaultMasterSecret ,
createAdminConfig : defaultAdminConfig ,
createTfState : defaultTfState ,
flags : applyFlags { } ,
wantPhases : newPhases ( skipInitPhase ) ,
} ,
"[upgrade] aws: all files exist" : {
createConfig : defaultConfig ( cloudprovider . AWS ) ,
createState : postInitState ( cloudprovider . AWS ) ,
createMasterSecret : defaultMasterSecret ,
createAdminConfig : defaultAdminConfig ,
createTfState : defaultTfState ,
flags : applyFlags { } ,
wantPhases : newPhases ( skipInitPhase ) ,
} ,
"[upgrade] azure: all files exist" : {
createConfig : defaultConfig ( cloudprovider . Azure ) ,
createState : postInitState ( cloudprovider . Azure ) ,
createMasterSecret : defaultMasterSecret ,
createAdminConfig : defaultAdminConfig ,
createTfState : defaultTfState ,
flags : applyFlags { } ,
wantPhases : newPhases ( skipInitPhase ) ,
} ,
"[upgrade] qemu: all files exist" : {
createConfig : defaultConfig ( cloudprovider . QEMU ) ,
createState : postInitState ( cloudprovider . QEMU ) ,
createMasterSecret : defaultMasterSecret ,
createAdminConfig : defaultAdminConfig ,
createTfState : defaultTfState ,
flags : applyFlags { } ,
wantPhases : newPhases ( skipInitPhase , skipImagePhase ) , // No image upgrades on QEMU
} ,
"no config file errors" : {
createConfig : func ( require * require . Assertions , fh file . Handler ) { } ,
createState : postInitState ( cloudprovider . GCP ) ,
createMasterSecret : defaultMasterSecret ,
createAdminConfig : defaultAdminConfig ,
createTfState : defaultTfState ,
flags : applyFlags { } ,
wantErr : true ,
} ,
"[init] no admin config file, but mastersecret file exists errors" : {
createConfig : defaultConfig ( cloudprovider . GCP ) ,
createState : preInitState ( cloudprovider . GCP ) ,
createMasterSecret : defaultMasterSecret ,
createAdminConfig : func ( require * require . Assertions , fh file . Handler ) { } ,
createTfState : defaultTfState ,
flags : applyFlags { } ,
wantErr : true ,
} ,
"[init] no admin config file, no master secret" : {
createConfig : defaultConfig ( cloudprovider . GCP ) ,
createState : preInitState ( cloudprovider . GCP ) ,
createMasterSecret : func ( require * require . Assertions , fh file . Handler ) { } ,
createAdminConfig : func ( require * require . Assertions , fh file . Handler ) { } ,
createTfState : defaultTfState ,
flags : applyFlags { } ,
wantPhases : newPhases ( skipImagePhase , skipK8sPhase ) ,
} ,
"[create] no tf state, but admin config exists errors" : {
createConfig : defaultConfig ( cloudprovider . GCP ) ,
createState : preInitState ( cloudprovider . GCP ) ,
createMasterSecret : defaultMasterSecret ,
createAdminConfig : defaultAdminConfig ,
createTfState : func ( require * require . Assertions , fh file . Handler ) { } ,
flags : applyFlags { } ,
wantErr : true ,
} ,
"[create] only config, skip everything but infrastructure" : {
createConfig : defaultConfig ( cloudprovider . GCP ) ,
createState : func ( require * require . Assertions , fh file . Handler ) { } ,
createMasterSecret : func ( require * require . Assertions , fh file . Handler ) { } ,
createAdminConfig : func ( require * require . Assertions , fh file . Handler ) { } ,
createTfState : func ( require * require . Assertions , fh file . Handler ) { } ,
flags : applyFlags {
skipPhases : newPhases ( skipInitPhase , skipAttestationConfigPhase , skipCertSANsPhase , skipHelmPhase , skipK8sPhase , skipImagePhase ) ,
} ,
wantPhases : newPhases ( skipInitPhase , skipAttestationConfigPhase , skipCertSANsPhase , skipHelmPhase , skipK8sPhase , skipImagePhase ) ,
} ,
"[create + init] only config file" : {
createConfig : defaultConfig ( cloudprovider . GCP ) ,
createState : func ( require * require . Assertions , fh file . Handler ) { } ,
createMasterSecret : func ( require * require . Assertions , fh file . Handler ) { } ,
createAdminConfig : func ( require * require . Assertions , fh file . Handler ) { } ,
createTfState : func ( require * require . Assertions , fh file . Handler ) { } ,
flags : applyFlags { } ,
wantPhases : newPhases ( skipImagePhase , skipK8sPhase ) ,
} ,
"[init] self-managed: config and state file exist, skip-phases=infrastructure" : {
createConfig : defaultConfig ( cloudprovider . GCP ) ,
createState : preInitState ( cloudprovider . GCP ) ,
createMasterSecret : func ( require * require . Assertions , fh file . Handler ) { } ,
createAdminConfig : func ( require * require . Assertions , fh file . Handler ) { } ,
createTfState : func ( require * require . Assertions , fh file . Handler ) { } ,
flags : applyFlags {
skipPhases : newPhases ( skipInfrastructurePhase ) ,
} ,
wantPhases : newPhases ( skipInfrastructurePhase , skipImagePhase , skipK8sPhase ) ,
} ,
2023-12-15 09:45:52 -05:00
"[upgrade] k8s patch version no longer supported, user confirms to skip k8s and continue upgrade. Valid K8s patch version is used in config afterwards" : {
createConfig : func ( require * require . Assertions , fh file . Handler ) {
cfg := defaultConfigWithExpectedMeasurements ( t , config . Default ( ) , cloudprovider . GCP )
// use first version in list (oldest) as it should never have a patch version
versionParts := strings . Split ( versions . SupportedK8sVersions ( ) [ 0 ] , "." )
versionParts [ len ( versionParts ) - 1 ] = "0"
cfg . KubernetesVersion = versions . ValidK8sVersion ( strings . Join ( versionParts , "." ) )
require . NoError ( fh . WriteYAML ( constants . ConfigFilename , cfg ) )
} ,
createState : postInitState ( cloudprovider . GCP ) ,
createMasterSecret : defaultMasterSecret ,
createAdminConfig : defaultAdminConfig ,
createTfState : defaultTfState ,
stdin : "y\n" ,
wantPhases : newPhases ( skipInitPhase , skipK8sPhase ) ,
assert : func ( require * require . Assertions , assert * assert . Assertions , conf * config . Config , stateFile * state . State ) {
assert . NotEmpty ( conf . KubernetesVersion )
_ , err := versions . NewValidK8sVersion ( string ( conf . KubernetesVersion ) , true )
assert . NoError ( err )
} ,
} ,
2023-11-20 05:17:16 -05:00
}
for name , tc := range testCases {
t . Run ( name , func ( t * testing . T ) {
assert := assert . New ( t )
require := require . New ( t )
fileHandler := file . NewHandler ( afero . NewMemMapFs ( ) )
tc . createConfig ( require , fileHandler )
tc . createState ( require , fileHandler )
tc . createMasterSecret ( require , fileHandler )
tc . createAdminConfig ( require , fileHandler )
tc . createTfState ( require , fileHandler )
cmd := NewApplyCmd ( )
var out bytes . Buffer
cmd . SetOut ( & out )
var errOut bytes . Buffer
cmd . SetErr ( & errOut )
cmd . SetIn ( bytes . NewBufferString ( tc . stdin ) )
a := applyCmd {
log : logger . NewTest ( t ) ,
fileHandler : fileHandler ,
flags : tc . flags ,
}
2023-12-15 09:45:52 -05:00
conf , state , err := a . validateInputs ( cmd , & stubAttestationFetcher { } )
2023-11-20 05:17:16 -05:00
if tc . wantErr {
assert . Error ( err )
return
}
assert . NoError ( err )
var cfgErr * config . ValidationError
if errors . As ( err , & cfgErr ) {
t . Log ( cfgErr . LongMessage ( ) )
}
assert . Equal ( tc . wantPhases , a . flags . skipPhases )
2023-12-15 09:45:52 -05:00
if tc . assert != nil {
tc . assert ( require , assert , conf , state )
}
2023-11-20 05:17:16 -05:00
} )
}
}
func TestSkipPhasesCompletion ( t * testing . T ) {
testCases := map [ string ] struct {
toComplete string
wantSuggestions [ ] string
} {
"empty" : {
toComplete : "" ,
wantSuggestions : allPhases ( ) ,
} ,
"partial" : {
toComplete : "hel" ,
wantSuggestions : [ ] string { string ( skipHelmPhase ) } ,
} ,
"one full word" : {
toComplete : string ( skipHelmPhase ) ,
} ,
"one full word with comma" : {
toComplete : string ( skipHelmPhase ) + "," ,
wantSuggestions : func ( ) [ ] string {
allPhases := allPhases ( )
var suggestions [ ] string
for _ , phase := range allPhases {
if phase == string ( skipHelmPhase ) {
continue
}
suggestions = append ( suggestions , fmt . Sprintf ( "%s,%s" , skipHelmPhase , phase ) )
}
return suggestions
} ( ) ,
} ,
"one full word, one partial" : {
toComplete : string ( skipHelmPhase ) + ",ima" ,
wantSuggestions : [ ] string { fmt . Sprintf ( "%s,%s" , skipHelmPhase , skipImagePhase ) } ,
} ,
"all phases" : {
toComplete : strings . Join ( allPhases ( ) , "," ) ,
wantSuggestions : [ ] string { } ,
} ,
}
for name , tc := range testCases {
t . Run ( name , func ( t * testing . T ) {
assert := assert . New ( t )
suggestions , _ := skipPhasesCompletion ( nil , nil , tc . toComplete )
assert . ElementsMatch ( tc . wantSuggestions , suggestions , "got: %v, want: %v" , suggestions , tc . wantSuggestions )
} )
}
}
func newPhases ( phases ... skipPhase ) skipPhases {
skipPhases := skipPhases { }
skipPhases . add ( phases ... )
return skipPhases
2023-10-30 04:30:35 -04:00
}
2023-12-01 02:37:52 -05:00
2023-12-04 07:40:24 -05:00
type stubConstellApplier struct {
checkLicenseErr error
2023-12-05 10:23:31 -05:00
masterSecret uri . MasterSecret
measurementSalt [ ] byte
2023-12-04 07:40:24 -05:00
generateMasterSecretErr error
generateMeasurementSaltErr error
initErr error
2023-12-11 09:55:44 -05:00
initOutput constellation . InitOutput
2023-12-05 10:23:31 -05:00
* stubKubernetesUpgrader
2023-12-06 04:01:39 -05:00
helmApplier
2023-12-04 07:40:24 -05:00
}
2023-12-01 02:37:52 -05:00
2023-12-05 10:23:31 -05:00
func ( s * stubConstellApplier ) SetKubeConfig ( [ ] byte ) error { return nil }
2023-12-22 04:16:36 -05:00
func ( s * stubConstellApplier ) CheckLicense ( context . Context , cloudprovider . Provider , bool , string ) ( int , error ) {
2023-12-04 07:40:24 -05:00
return 0 , s . checkLicenseErr
}
func ( s * stubConstellApplier ) GenerateMasterSecret ( ) ( uri . MasterSecret , error ) {
2023-12-05 10:23:31 -05:00
return s . masterSecret , s . generateMasterSecretErr
2023-12-04 07:40:24 -05:00
}
func ( s * stubConstellApplier ) GenerateMeasurementSalt ( ) ( [ ] byte , error ) {
2023-12-05 10:23:31 -05:00
return s . measurementSalt , s . generateMeasurementSaltErr
2023-12-04 07:40:24 -05:00
}
2023-12-11 09:55:44 -05:00
func ( s * stubConstellApplier ) Init ( context . Context , atls . Validator , * state . State , io . Writer , constellation . InitPayload ) ( constellation . InitOutput , error ) {
return s . initOutput , s . initErr
2023-12-01 02:37:52 -05:00
}
2023-12-06 04:01:39 -05:00
type helmApplier interface {
PrepareHelmCharts (
flags helm . Options , stateFile * state . State , serviceAccURI string , masterSecret uri . MasterSecret , openStackCfg * config . OpenStackConfig ,
) (
helm . Applier , bool , error )
}