2023-10-24 09:39:18 -04:00
/ *
Copyright ( c ) Edgeless Systems GmbH
SPDX - License - Identifier : AGPL - 3.0 - only
* /
package cmd
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"io/fs"
2024-02-21 10:39:12 -05:00
"log/slog"
2023-10-24 09:39:18 -04:00
"net"
2024-02-21 10:39:12 -05:00
"os"
2023-10-24 09:39:18 -04:00
"path/filepath"
2023-10-30 04:30:35 -04:00
"slices"
2023-10-24 09:39:18 -04:00
"strings"
"time"
"github.com/edgelesssys/constellation/v2/cli/internal/cloudcmd"
"github.com/edgelesssys/constellation/v2/internal/api/attestationconfigapi"
2023-12-05 10:23:31 -05:00
"github.com/edgelesssys/constellation/v2/internal/api/versionsapi"
2023-10-24 09:39:18 -04:00
"github.com/edgelesssys/constellation/v2/internal/atls"
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
"github.com/edgelesssys/constellation/v2/internal/cloud/cloudprovider"
"github.com/edgelesssys/constellation/v2/internal/compatibility"
"github.com/edgelesssys/constellation/v2/internal/config"
"github.com/edgelesssys/constellation/v2/internal/constants"
2023-12-01 02:37:52 -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-05 10:23:31 -05:00
"github.com/edgelesssys/constellation/v2/internal/constellation/kubecmd"
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"
"github.com/edgelesssys/constellation/v2/internal/grpc/dialer"
2023-12-05 10:23:31 -05:00
"github.com/edgelesssys/constellation/v2/internal/imagefetcher"
2023-12-04 07:40:24 -05:00
"github.com/edgelesssys/constellation/v2/internal/kms/uri"
2023-12-05 10:23:31 -05:00
"github.com/edgelesssys/constellation/v2/internal/semver"
2023-10-24 09:39:18 -04:00
"github.com/edgelesssys/constellation/v2/internal/versions"
2024-03-01 11:06:02 -05:00
slogmulti "github.com/samber/slog-multi"
2023-10-24 09:39:18 -04:00
"github.com/spf13/afero"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
2023-12-15 09:45:52 -05:00
xsemver "golang.org/x/mod/semver"
2023-12-05 10:23:31 -05:00
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
2023-10-24 09:39:18 -04:00
k8serrors "k8s.io/apimachinery/pkg/api/errors"
)
2023-10-26 09:59:13 -04:00
// phases that can be skipped during apply.
2023-10-30 04:30:35 -04:00
// New phases should also be added to [allPhases].
2023-10-26 09:59:13 -04:00
const (
// skipInfrastructurePhase skips the Terraform apply of the apply process.
skipInfrastructurePhase skipPhase = "infrastructure"
// skipInitPhase skips the init RPC of the apply process.
skipInitPhase skipPhase = "init"
// skipAttestationConfigPhase skips the attestation config upgrade of the apply process.
skipAttestationConfigPhase skipPhase = "attestationconfig"
// skipCertSANsPhase skips the cert SANs upgrade of the apply process.
skipCertSANsPhase skipPhase = "certsans"
// skipHelmPhase skips the helm upgrade of the apply process.
skipHelmPhase skipPhase = "helm"
// skipImagePhase skips the image upgrade of the apply process.
skipImagePhase skipPhase = "image"
// skipK8sPhase skips the Kubernetes version upgrade of the apply process.
skipK8sPhase skipPhase = "k8s"
)
2023-10-30 04:30:35 -04:00
// allPhases returns a list of all phases that can be skipped as strings.
2023-11-20 05:17:16 -05:00
func allPhases ( except ... skipPhase ) [ ] string {
phases := [ ] string {
2023-10-26 09:59:13 -04:00
string ( skipInfrastructurePhase ) ,
string ( skipInitPhase ) ,
string ( skipAttestationConfigPhase ) ,
string ( skipCertSANsPhase ) ,
string ( skipHelmPhase ) ,
string ( skipImagePhase ) ,
string ( skipK8sPhase ) ,
2023-10-30 04:30:35 -04:00
}
2023-11-20 05:17:16 -05:00
var returnedPhases [ ] string
for idx , phase := range phases {
if ! slices . Contains ( except , skipPhase ( phase ) ) {
returnedPhases = append ( returnedPhases , phases [ idx ] )
}
}
return returnedPhases
2023-10-30 04:30:35 -04:00
}
// formatSkipPhases returns a formatted string of all phases that can be skipped.
func formatSkipPhases ( ) string {
return fmt . Sprintf ( "{ %s }" , strings . Join ( allPhases ( ) , " | " ) )
2023-10-26 09:59:13 -04:00
}
// skipPhase is a phase of the upgrade process that can be skipped.
type skipPhase string
// skipPhases is a list of phases that can be skipped during the upgrade process.
type skipPhases map [ skipPhase ] struct { }
2023-11-20 05:17:16 -05:00
// contains returns true if skipPhases contains all of the given phases.
func ( s skipPhases ) contains ( phases ... skipPhase ) bool {
for _ , phase := range phases {
if _ , ok := s [ skipPhase ( strings . ToLower ( string ( phase ) ) ) ] ; ! ok {
return false
}
}
return true
2023-10-26 09:59:13 -04:00
}
// add a phase to the list of phases.
func ( s * skipPhases ) add ( phases ... skipPhase ) {
if * s == nil {
* s = make ( skipPhases )
}
for _ , phase := range phases {
( * s ) [ skipPhase ( strings . ToLower ( string ( phase ) ) ) ] = struct { } { }
}
}
// NewApplyCmd creates the apply command.
func NewApplyCmd ( ) * cobra . Command {
cmd := & cobra . Command {
Use : "apply" ,
Short : "Apply a configuration to a Constellation cluster" ,
Long : "Apply a configuration to a Constellation cluster to initialize or upgrade the cluster." ,
Args : cobra . NoArgs ,
RunE : runApply ,
}
cmd . Flags ( ) . Bool ( "conformance" , false , "enable conformance mode" )
cmd . Flags ( ) . Bool ( "skip-helm-wait" , false , "install helm charts without waiting for deployments to be ready" )
cmd . Flags ( ) . Bool ( "merge-kubeconfig" , false , "merge Constellation kubeconfig file with default kubeconfig file in $HOME/.kube/config" )
cmd . Flags ( ) . BoolP ( "yes" , "y" , false , "run command without further confirmation\n" +
"WARNING: the command might delete or update existing resources without additional checks. Please read the docs.\n" )
2023-11-29 08:55:10 -05:00
cmd . Flags ( ) . Duration ( "helm-timeout" , 10 * time . Minute , "change helm install/upgrade timeout\n" +
2023-10-26 09:59:13 -04:00
"Might be useful for slow connections or big clusters." )
cmd . Flags ( ) . StringSlice ( "skip-phases" , nil , "comma-separated list of upgrade phases to skip\n" +
fmt . Sprintf ( "one or multiple of %s" , formatSkipPhases ( ) ) )
2023-11-29 08:55:10 -05:00
must ( cmd . Flags ( ) . MarkHidden ( "helm-timeout" ) )
2023-10-26 09:59:13 -04:00
2023-11-20 05:17:16 -05:00
must ( cmd . RegisterFlagCompletionFunc ( "skip-phases" , skipPhasesCompletion ) )
2023-10-26 09:59:13 -04:00
return cmd
}
2023-10-24 09:39:18 -04:00
// applyFlags defines the flags for the apply command.
type applyFlags struct {
rootFlags
2023-11-29 08:55:10 -05:00
yes bool
conformance bool
mergeConfigs bool
helmTimeout time . Duration
helmWaitMode helm . WaitMode
skipPhases skipPhases
2023-10-24 09:39:18 -04:00
}
// parse the apply command flags.
func ( f * applyFlags ) parse ( flags * pflag . FlagSet ) error {
if err := f . rootFlags . parse ( flags ) ; err != nil {
return err
}
rawSkipPhases , err := flags . GetStringSlice ( "skip-phases" )
if err != nil {
return fmt . Errorf ( "getting 'skip-phases' flag: %w" , err )
}
var skipPhases skipPhases
for _ , phase := range rawSkipPhases {
2023-10-30 04:30:35 -04:00
phase = strings . ToLower ( phase )
if slices . Contains ( allPhases ( ) , phase ) {
2023-10-24 09:39:18 -04:00
skipPhases . add ( skipPhase ( phase ) )
2023-10-30 04:30:35 -04:00
} else {
2023-10-24 09:39:18 -04:00
return fmt . Errorf ( "invalid phase %s" , phase )
}
}
f . skipPhases = skipPhases
f . yes , err = flags . GetBool ( "yes" )
if err != nil {
return fmt . Errorf ( "getting 'yes' flag: %w" , err )
}
2023-11-29 08:55:10 -05:00
f . helmTimeout , err = flags . GetDuration ( "helm-timeout" )
2023-10-24 09:39:18 -04:00
if err != nil {
2023-11-29 08:55:10 -05:00
return fmt . Errorf ( "getting 'helm-timeout' flag: %w" , err )
2023-10-24 09:39:18 -04:00
}
f . conformance , err = flags . GetBool ( "conformance" )
if err != nil {
return fmt . Errorf ( "getting 'conformance' flag: %w" , err )
}
skipHelmWait , err := flags . GetBool ( "skip-helm-wait" )
if err != nil {
return fmt . Errorf ( "getting 'skip-helm-wait' flag: %w" , err )
}
f . helmWaitMode = helm . WaitModeAtomic
if skipHelmWait {
f . helmWaitMode = helm . WaitModeNone
}
f . mergeConfigs , err = flags . GetBool ( "merge-kubeconfig" )
if err != nil {
return fmt . Errorf ( "getting 'merge-kubeconfig' flag: %w" , err )
}
return nil
}
// runApply sets up the apply command and runs it.
func runApply ( cmd * cobra . Command , _ [ ] string ) error {
log , err := newCLILogger ( cmd )
if err != nil {
return fmt . Errorf ( "creating logger: %w" , err )
}
spinner , err := newSpinnerOrStderr ( cmd )
if err != nil {
return err
}
defer spinner . Stop ( )
flags := applyFlags { }
if err := flags . parse ( cmd . Flags ( ) ) ; err != nil {
return err
}
fileHandler := file . NewHandler ( afero . NewOsFs ( ) )
2024-02-21 10:39:12 -05:00
logger , err := newDebugFileLogger ( cmd , fileHandler )
if err != nil {
return err
}
2023-10-24 09:39:18 -04:00
newDialer := func ( validator atls . Validator ) * dialer . Dialer {
return dialer . New ( nil , validator , & net . Dialer { } )
}
2023-12-05 10:23:31 -05:00
2023-10-24 09:39:18 -04:00
upgradeID := generateUpgradeID ( upgradeCmdKindApply )
upgradeDir := filepath . Join ( constants . UpgradeDir , upgradeID )
2023-10-30 07:43:38 -04:00
2023-10-31 07:46:40 -04:00
newInfraApplier := func ( ctx context . Context ) ( cloudApplier , func ( ) , error ) {
return cloudcmd . NewApplier (
2023-10-30 07:43:38 -04:00
ctx ,
2023-10-31 07:46:40 -04:00
spinner ,
2023-10-30 07:43:38 -04:00
constants . TerraformWorkingDir ,
upgradeDir ,
flags . tfLogLevel ,
fileHandler ,
)
2023-10-24 09:39:18 -04:00
}
2023-12-22 04:16:36 -05:00
applier := constellation . NewApplier ( log , spinner , constellation . ApplyContextCLI , newDialer )
2023-12-01 02:37:52 -05:00
2023-10-24 09:39:18 -04:00
apply := & applyCmd {
2023-10-31 07:46:40 -04:00
fileHandler : fileHandler ,
flags : flags ,
2024-02-21 10:39:12 -05:00
log : logger ,
2023-12-05 06:28:40 -05:00
wLog : & warnLogger { cmd : cmd , log : log } ,
2023-10-31 07:46:40 -04:00
spinner : spinner ,
merger : & kubeconfigMerger { log : log } ,
newInfraApplier : newInfraApplier ,
2023-12-05 10:23:31 -05:00
imageFetcher : imagefetcher . New ( ) ,
2023-12-01 02:37:52 -05:00
applier : applier ,
2023-10-24 09:39:18 -04:00
}
ctx , cancel := context . WithTimeout ( cmd . Context ( ) , time . Hour )
defer cancel ( )
cmd . SetContext ( ctx )
2023-12-01 02:37:52 -05:00
return apply . apply ( cmd , attestationconfigapi . NewFetcher ( ) , upgradeDir )
2023-10-24 09:39:18 -04:00
}
type applyCmd struct {
fileHandler file . Handler
flags applyFlags
log debugLog
2023-12-04 07:40:24 -05:00
wLog warnLog
2023-10-24 09:39:18 -04:00
spinner spinnerInterf
2023-11-20 05:17:16 -05:00
merger configMerger
2023-10-24 09:39:18 -04:00
2023-12-05 10:23:31 -05:00
imageFetcher imageFetcher
applier applier
2023-12-01 02:37:52 -05:00
2023-10-31 07:46:40 -04:00
newInfraApplier func ( context . Context ) ( cloudApplier , func ( ) , error )
2023-10-24 09:39:18 -04:00
}
/ *
apply updates a Constellation cluster by applying a user ' s config .
The control flow is as follows :
┌ ─ ─ ─ ─ ─ ─ ─ ▼ ─ ─ ─ ─ ─ ─ ─ ┐
│ Parse Flags │
│ │
│ Read Config │
│ │
│ Read State - File │
│ │
│ Validate input │
└ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ┘
│ ─ ─ ─ ┐
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ▼ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │
│ Check if Terraform state is up to date │ │
└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │
│ │ Not up to date │
│ │ ( Diff from Terraform plan ) │
│ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │
2023-10-26 09:59:13 -04:00
│ │ | Infrastructure
2023-10-24 09:39:18 -04:00
│ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ▼ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │ Phase
│ │ Apply Terraform updates │ │
│ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │
│ │ │
│ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │
│ │ ─ ─ ─ ┘
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ▼ ─ ─ ▼ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
│ Check for constellation - admin . conf │
└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘
File does not exist │ │
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ ─ ─ ─ ┐
│ │ │
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ▼ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │ │
│ Run Bootstrapper Init RPC │ │ │
└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ File does exist │
│ │ │
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ▼ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │ │ Init
│ Write constellation - admin . conf │ │ │ Phase
└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ │
│ │ │
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ▼ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │ │
│ Prepare "Init Success" Message │ │ │
└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ │
│ │ │
└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │ ─ ─ ─ ┘
2023-10-26 09:59:13 -04:00
│ │ ─ ─ ─ ┐
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ▼ ─ ─ ▼ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │ AttestationConfig
│ Apply Attestation Config │ │ Phase
└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ ─ ─ ─ ┘
│ ─ ─ ─ ┐
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ▼ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │ CertSANs
│ Extend API Server Cert SANs │ │ Phase
└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ ─ ─ ─ ┘
2023-10-24 09:39:18 -04:00
│ ─ ─ ─ ┐
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ▼ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │ Helm
│ Apply Helm Charts │ │ Phase
└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ┘ ─ ─ ─ ┘
│ ─ ─ ─ ┐
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ▼ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │
Can be skipped │ Upgrade NodeVersion object │ │ K8s / Image
2023-12-01 02:37:52 -05:00
if we ran Init RPC │ ( Image and K8s update ) │ │ Phase
2023-10-24 09:39:18 -04:00
└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │
│ ─ ─ ─ ┘
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ▼ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
│ Write success output │
└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘
* /
2023-11-20 05:17:16 -05:00
func ( a * applyCmd ) apply (
2023-12-01 02:37:52 -05:00
cmd * cobra . Command , configFetcher attestationconfigapi . Fetcher , upgradeDir string ,
2023-11-20 05:17:16 -05:00
) error {
2023-10-24 09:39:18 -04:00
// Validate inputs
conf , stateFile , err := a . validateInputs ( cmd , configFetcher )
if err != nil {
return err
}
2023-11-20 05:17:16 -05:00
// Check license
2024-03-01 11:06:02 -05:00
a . checkLicenseFile ( cmd , conf . GetProvider ( ) , conf . UseMarketplaceImage ( ) )
2023-11-20 05:17:16 -05:00
2023-10-24 09:39:18 -04:00
// Now start actually running the apply command
// Check current Terraform state, if it exists and infrastructure upgrades are not skipped,
// and apply migrations if necessary.
if ! a . flags . skipPhases . contains ( skipInfrastructurePhase ) {
if err := a . runTerraformApply ( cmd , conf , stateFile , upgradeDir ) ; err != nil {
2023-11-20 05:17:16 -05:00
return fmt . Errorf ( "applying Terraform configuration: %w" , err )
2023-10-24 09:39:18 -04:00
}
}
bufferedOutput := & bytes . Buffer { }
// Run init RPC if required
if ! a . flags . skipPhases . contains ( skipInitPhase ) {
bufferedOutput , err = a . runInit ( cmd , conf , stateFile )
if err != nil {
return err
}
}
2023-12-05 10:23:31 -05:00
if a . flags . skipPhases . contains ( skipAttestationConfigPhase , skipCertSANsPhase , skipHelmPhase , skipK8sPhase , skipImagePhase ) {
cmd . Print ( bufferedOutput . String ( ) )
return nil
}
2023-10-24 09:39:18 -04:00
// From now on we can assume a valid Kubernetes admin config file exists
2023-12-05 10:23:31 -05:00
kubeConfig , err := a . fileHandler . Read ( constants . AdminConfFilename )
if err != nil {
return fmt . Errorf ( "reading kubeconfig: %w" , err )
}
if err := a . applier . SetKubeConfig ( kubeConfig ) ; err != nil {
return err
2023-10-24 09:39:18 -04:00
}
// Apply Attestation Config
2023-10-26 09:59:13 -04:00
if ! a . flags . skipPhases . contains ( skipAttestationConfigPhase ) {
2024-02-08 09:20:01 -05:00
a . log . Debug ( "Applying new attestation config to cluster" )
2023-12-05 10:23:31 -05:00
if err := a . applyJoinConfig ( cmd , conf . GetAttestationConfig ( ) , stateFile . ClusterValues . MeasurementSalt ) ; err != nil {
2023-10-26 09:59:13 -04:00
return fmt . Errorf ( "applying attestation config: %w" , err )
}
2023-10-24 09:39:18 -04:00
}
// Extend API Server Cert SANs
2023-10-26 09:59:13 -04:00
if ! a . flags . skipPhases . contains ( skipCertSANsPhase ) {
2023-12-05 10:23:31 -05:00
if err := a . applier . ExtendClusterConfigCertSANs (
cmd . Context ( ) ,
stateFile . Infrastructure . ClusterEndpoint ,
conf . CustomEndpoint ,
stateFile . Infrastructure . APIServerCertSANs ,
) ; err != nil {
2023-10-26 09:59:13 -04:00
return fmt . Errorf ( "extending cert SANs: %w" , err )
}
2023-10-24 09:39:18 -04:00
}
// Apply Helm Charts
if ! a . flags . skipPhases . contains ( skipHelmPhase ) {
2024-07-03 13:37:51 -04:00
if err := a . applier . AnnotateCoreDNSResources ( cmd . Context ( ) ) ; err != nil {
return fmt . Errorf ( "annotating CoreDNS: %w" , err )
}
2023-12-05 10:23:31 -05:00
if err := a . runHelmApply ( cmd , conf , stateFile , upgradeDir ) ; err != nil {
2023-10-24 09:39:18 -04:00
return err
}
}
2023-12-05 10:23:31 -05:00
// Upgrade node image
if ! a . flags . skipPhases . contains ( skipImagePhase ) {
if err := a . runNodeImageUpgrade ( cmd , conf ) ; err != nil {
return err
}
}
// Upgrade Kubernetes version
if ! a . flags . skipPhases . contains ( skipK8sPhase ) {
if err := a . runK8sVersionUpgrade ( cmd , conf ) ; err != nil {
2023-10-24 09:39:18 -04:00
return err
}
}
// Write success output
cmd . Print ( bufferedOutput . String ( ) )
return nil
}
func ( a * applyCmd ) validateInputs ( cmd * cobra . Command , configFetcher attestationconfigapi . Fetcher ) ( * config . Config , * state . State , error ) {
// Read user's config and state file
2024-04-03 09:49:03 -04:00
a . log . Debug ( fmt . Sprintf ( "Reading config from %q" , a . flags . pathPrefixer . PrefixPrintablePath ( constants . ConfigFilename ) ) )
2023-10-24 09:39:18 -04:00
conf , err := config . New ( a . fileHandler , constants . ConfigFilename , configFetcher , a . flags . force )
var configValidationErr * config . ValidationError
if errors . As ( err , & configValidationErr ) {
cmd . PrintErrln ( configValidationErr . LongMessage ( ) )
}
if err != nil {
return nil , nil , err
}
2024-04-03 09:49:03 -04:00
a . log . Debug ( fmt . Sprintf ( "Reading state file from %q" , a . flags . pathPrefixer . PrefixPrintablePath ( constants . StateFilename ) ) )
2023-11-20 05:17:16 -05:00
stateFile , err := state . CreateOrRead ( a . fileHandler , constants . StateFilename )
if err != nil {
return nil , nil , err
2023-10-24 09:39:18 -04:00
}
2023-11-20 05:17:16 -05:00
// Validate the state file and set flags accordingly
//
// We don't run "hard" verification of skip-phases flags and state file here,
// a user may still end up skipping phases that could result in errors later on.
// However, we perform basic steps, like ensuring init phase is not skipped if
2024-02-08 09:20:01 -05:00
a . log . Debug ( "Validating state file" )
2024-01-24 09:10:15 -05:00
preCreateValidateErr := stateFile . Validate ( state . PreCreate , conf . GetAttestationConfig ( ) . GetVariant ( ) )
preInitValidateErr := stateFile . Validate ( state . PreInit , conf . GetAttestationConfig ( ) . GetVariant ( ) )
postInitValidateErr := stateFile . Validate ( state . PostInit , conf . GetAttestationConfig ( ) . GetVariant ( ) )
2023-11-20 05:17:16 -05:00
// If the state file is in a pre-create state, we need to create the cluster,
// in which case the workspace has to be clean
if preCreateValidateErr == nil {
// We can't skip the infrastructure phase if no infrastructure has been defined
2024-02-08 09:20:01 -05:00
a . log . Debug ( "State file is in pre-create state, checking workspace" )
2023-11-20 05:17:16 -05:00
if a . flags . skipPhases . contains ( skipInfrastructurePhase ) {
return nil , nil , preInitValidateErr
}
if err := a . checkCreateFilesClean ( ) ; err != nil {
return nil , nil , err
}
2024-02-08 09:20:01 -05:00
a . log . Debug ( "No Terraform state found in current working directory. Preparing to create a new cluster." )
2023-11-20 05:17:16 -05:00
printCreateWarnings ( cmd . ErrOrStderr ( ) , conf )
2023-10-24 09:39:18 -04:00
}
2023-11-20 05:17:16 -05:00
// Check if the state file is in a pre-init OR
// if in pre-create state and init should not be skipped
// If so, we need to run the init RPC
if preInitValidateErr == nil || ( preCreateValidateErr == nil && ! a . flags . skipPhases . contains ( skipInitPhase ) ) {
// We can't skip the init phase if the init RPC hasn't been run yet
2024-02-08 09:20:01 -05:00
a . log . Debug ( "State file is in pre-init state, checking workspace" )
2023-11-20 05:17:16 -05:00
if a . flags . skipPhases . contains ( skipInitPhase ) {
return nil , nil , postInitValidateErr
}
if err := a . checkInitFilesClean ( ) ; err != nil {
return nil , nil , err
}
// Skip image and k8s phase, since they are covered by the init RPC
a . flags . skipPhases . add ( skipImagePhase , skipK8sPhase )
}
// If the state file is in a post-init state,
// we need to make sure specific files exist in the workspace
if postInitValidateErr == nil {
2024-02-08 09:20:01 -05:00
a . log . Debug ( "State file is in post-init state, checking workspace" )
2023-11-20 05:17:16 -05:00
if err := a . checkPostInitFilesExist ( ) ; err != nil {
return nil , nil , err
}
// Skip init phase, since the init RPC has already been run
a . flags . skipPhases . add ( skipInitPhase )
} else if preCreateValidateErr != nil && preInitValidateErr != nil {
return nil , nil , postInitValidateErr
}
2023-10-24 09:39:18 -04:00
// Validate Kubernetes version as set in the user's config
// If we need to run the init RPC, the version has to be valid
// Otherwise, we are able to use an outdated version, meaning we skip the K8s upgrade
// We skip version validation if the user explicitly skips the Kubernetes phase
2024-04-03 09:49:03 -04:00
a . log . Debug ( fmt . Sprintf ( "Validating Kubernetes version %q" , conf . KubernetesVersion ) )
2023-10-24 09:39:18 -04:00
validVersion , err := versions . NewValidK8sVersion ( string ( conf . KubernetesVersion ) , true )
2023-11-20 05:17:16 -05:00
if err != nil {
2024-04-03 09:49:03 -04:00
a . log . Debug ( fmt . Sprintf ( "Kubernetes version not valid: %q" , err ) )
2023-10-24 09:39:18 -04:00
if ! a . flags . skipPhases . contains ( skipInitPhase ) {
return nil , nil , err
}
2023-11-20 05:17:16 -05:00
if ! a . flags . skipPhases . contains ( skipK8sPhase ) {
2024-02-08 09:20:01 -05:00
a . log . Debug ( "Checking if user wants to continue anyway" )
2023-11-20 05:17:16 -05:00
if ! a . flags . yes {
confirmed , err := askToConfirm ( cmd ,
fmt . Sprintf (
"WARNING: The Kubernetes patch version %s is not supported. If you continue, Kubernetes upgrades will be skipped. Do you want to continue anyway?" ,
validVersion ,
) ,
)
if err != nil {
return nil , nil , fmt . Errorf ( "asking for confirmation: %w" , err )
}
if ! confirmed {
return nil , nil , fmt . Errorf ( "aborted by user" )
}
2023-10-24 09:39:18 -04:00
}
2023-12-15 09:45:52 -05:00
2023-11-20 05:17:16 -05:00
a . flags . skipPhases . add ( skipK8sPhase )
2024-02-08 09:20:01 -05:00
a . log . Debug ( "Outdated Kubernetes version accepted, Kubernetes upgrade will be skipped" )
2023-10-24 09:39:18 -04:00
}
2023-12-15 09:45:52 -05:00
validVersionString , err := versions . ResolveK8sPatchVersion ( xsemver . MajorMinor ( string ( conf . KubernetesVersion ) ) )
if err != nil {
return nil , nil , fmt . Errorf ( "resolving Kubernetes patch version: %w" , err )
}
validVersion , err = versions . NewValidK8sVersion ( validVersionString , true )
if err != nil {
return nil , nil , fmt . Errorf ( "parsing Kubernetes version: %w" , err )
}
2023-10-24 09:39:18 -04:00
}
if versions . IsPreviewK8sVersion ( validVersion ) {
cmd . PrintErrf ( "Warning: Constellation with Kubernetes %s is still in preview. Use only for evaluation purposes.\n" , validVersion )
}
conf . KubernetesVersion = validVersion
2024-04-03 09:49:03 -04:00
a . log . Debug ( fmt . Sprintf ( "Target Kubernetes version set to %q" , conf . KubernetesVersion ) )
2023-10-24 09:39:18 -04:00
// Validate microservice version (helm versions) in the user's config matches the version of the CLI
// This makes sure we catch potential errors early, not just after we already ran Terraform migrations or the init RPC
2023-11-20 05:17:16 -05:00
if ! a . flags . force && ! a . flags . skipPhases . contains ( skipHelmPhase , skipInitPhase ) {
2023-10-24 09:39:18 -04:00
if err := validateCLIandConstellationVersionAreEqual ( constants . BinaryVersion ( ) , conf . Image , conf . MicroserviceVersion ) ; err != nil {
return nil , nil , err
}
}
2023-11-20 05:17:16 -05:00
// Constellation does not support image upgrades on all CSPs. Not supported are: QEMU, OpenStack
// If using one of those providers, print a warning when trying to upgrade the image
if ! ( conf . GetProvider ( ) == cloudprovider . AWS || conf . GetProvider ( ) == cloudprovider . Azure || conf . GetProvider ( ) == cloudprovider . GCP ) &&
! a . flags . skipPhases . contains ( skipImagePhase ) {
cmd . PrintErrf ( "Image upgrades are not supported for provider %s\n" , conf . GetProvider ( ) )
cmd . PrintErrln ( "Image phase will be skipped" )
a . flags . skipPhases . add ( skipImagePhase )
2023-10-24 09:39:18 -04:00
}
return conf , stateFile , nil
}
2023-11-20 05:17:16 -05:00
// applyJoinConfig creates or updates the cluster's join config.
2023-10-24 09:39:18 -04:00
// If the config already exists, and is different from the new config, the user is asked to confirm the upgrade.
2023-12-05 10:23:31 -05:00
func ( a * applyCmd ) applyJoinConfig ( cmd * cobra . Command , newConfig config . AttestationCfg , measurementSalt [ ] byte ,
2023-10-24 09:39:18 -04:00
) error {
2023-12-05 10:23:31 -05:00
clusterAttestationConfig , err := a . applier . GetClusterAttestationConfig ( cmd . Context ( ) , newConfig . GetVariant ( ) )
2023-10-24 09:39:18 -04:00
if err != nil {
2024-04-03 09:49:03 -04:00
a . log . Debug ( fmt . Sprintf ( "Getting cluster attestation config failed: %q" , err ) )
2023-10-24 09:39:18 -04:00
if k8serrors . IsNotFound ( err ) {
2024-02-08 09:20:01 -05:00
a . log . Debug ( "Creating new join config" )
2023-12-05 10:23:31 -05:00
return a . applier . ApplyJoinConfig ( cmd . Context ( ) , newConfig , measurementSalt )
2023-10-24 09:39:18 -04:00
}
return fmt . Errorf ( "getting cluster attestation config: %w" , err )
}
// If the current config is equal, or there is an error when comparing the configs, we skip the upgrade.
equal , err := newConfig . EqualTo ( clusterAttestationConfig )
if err != nil {
return fmt . Errorf ( "comparing attestation configs: %w" , err )
}
if equal {
2024-02-08 09:20:01 -05:00
a . log . Debug ( "Current attestation config is equal to the new config, nothing to do" )
2023-10-24 09:39:18 -04:00
return nil
}
cmd . Println ( "The configured attestation config is different from the attestation config in the cluster." )
diffStr , err := diffAttestationCfg ( clusterAttestationConfig , newConfig )
if err != nil {
return fmt . Errorf ( "diffing attestation configs: %w" , err )
}
cmd . Println ( "The following changes will be applied to the attestation config:" )
cmd . Println ( diffStr )
if ! a . flags . yes {
ok , err := askToConfirm ( cmd , "Are you sure you want to change your cluster's attestation config?" )
if err != nil {
return fmt . Errorf ( "asking for confirmation: %w" , err )
}
if ! ok {
return errors . New ( "aborting upgrade since attestation config is different" )
}
}
2023-12-05 10:23:31 -05:00
if err := a . applier . ApplyJoinConfig ( cmd . Context ( ) , newConfig , measurementSalt ) ; err != nil {
2023-10-24 09:39:18 -04:00
return fmt . Errorf ( "updating attestation config: %w" , err )
}
cmd . Println ( "Successfully updated the cluster's attestation config" )
return nil
}
2023-12-05 10:23:31 -05:00
func ( a * applyCmd ) runNodeImageUpgrade ( cmd * cobra . Command , conf * config . Config ) error {
provider := conf . GetProvider ( )
attestationVariant := conf . GetAttestationConfig ( ) . GetVariant ( )
region := conf . GetRegion ( )
2023-12-08 08:40:31 -05:00
imageReference , err := a . imageFetcher . FetchReference ( cmd . Context ( ) , provider , attestationVariant , conf . Image , region , conf . UseMarketplaceImage ( ) )
2023-12-05 10:23:31 -05:00
if err != nil {
return fmt . Errorf ( "fetching image reference: %w" , err )
}
imageVersionInfo , err := versionsapi . NewVersionFromShortPath ( conf . Image , versionsapi . VersionKindImage )
if err != nil {
return fmt . Errorf ( "parsing version from image short path: %w" , err )
}
imageVersion , err := semver . New ( imageVersionInfo . Version ( ) )
if err != nil {
return fmt . Errorf ( "parsing image version: %w" , err )
}
2023-10-24 09:39:18 -04:00
2023-12-05 10:23:31 -05:00
err = a . applier . UpgradeNodeImage ( cmd . Context ( ) , imageVersion , imageReference , a . flags . force )
2023-10-24 09:39:18 -04:00
var upgradeErr * compatibility . InvalidUpgradeError
switch {
case errors . Is ( err , kubecmd . ErrInProgress ) :
2023-12-05 10:23:31 -05:00
cmd . PrintErrln ( "Skipping image upgrade: Another upgrade is already in progress." )
2023-10-24 09:39:18 -04:00
case errors . As ( err , & upgradeErr ) :
cmd . PrintErrln ( err )
case err != nil :
return fmt . Errorf ( "upgrading NodeVersion: %w" , err )
}
return nil
}
2023-12-05 10:23:31 -05:00
func ( a * applyCmd ) runK8sVersionUpgrade ( cmd * cobra . Command , conf * config . Config ) error {
err := a . applier . UpgradeKubernetesVersion ( cmd . Context ( ) , conf . KubernetesVersion , a . flags . force )
var upgradeErr * compatibility . InvalidUpgradeError
switch {
case errors . As ( err , & upgradeErr ) :
cmd . PrintErrln ( err )
case err != nil :
return fmt . Errorf ( "upgrading Kubernetes version: %w" , err )
}
return nil
}
2023-11-20 05:17:16 -05:00
// checkCreateFilesClean ensures that the workspace is clean before creating a new cluster.
func ( a * applyCmd ) checkCreateFilesClean ( ) error {
if err := a . checkInitFilesClean ( ) ; err != nil {
return err
}
2024-02-08 09:20:01 -05:00
a . log . Debug ( "Checking Terraform state" )
2023-11-20 05:17:16 -05:00
if _ , err := a . fileHandler . Stat ( constants . TerraformWorkingDir ) ; err == nil {
return fmt . Errorf (
"terraform state %q already exists in working directory, run 'constellation terminate' before creating a new cluster" ,
a . flags . pathPrefixer . PrefixPrintablePath ( constants . TerraformWorkingDir ) ,
)
} else if ! errors . Is ( err , fs . ErrNotExist ) {
return fmt . Errorf ( "checking for %s: %w" , a . flags . pathPrefixer . PrefixPrintablePath ( constants . TerraformWorkingDir ) , err )
}
return nil
}
// checkInitFilesClean ensures that the workspace is clean before running the init RPC.
func ( a * applyCmd ) checkInitFilesClean ( ) error {
2024-02-08 09:20:01 -05:00
a . log . Debug ( "Checking admin configuration file" )
2023-11-20 05:17:16 -05:00
if _ , err := a . fileHandler . Stat ( constants . AdminConfFilename ) ; err == nil {
return fmt . Errorf (
"file %q already exists in working directory, run 'constellation terminate' before creating a new cluster" ,
a . flags . pathPrefixer . PrefixPrintablePath ( constants . AdminConfFilename ) ,
)
} else if ! errors . Is ( err , fs . ErrNotExist ) {
return fmt . Errorf ( "checking for %q: %w" , a . flags . pathPrefixer . PrefixPrintablePath ( constants . AdminConfFilename ) , err )
}
2024-02-08 09:20:01 -05:00
a . log . Debug ( "Checking master secrets file" )
2023-11-20 05:17:16 -05:00
if _ , err := a . fileHandler . Stat ( constants . MasterSecretFilename ) ; err == nil {
return fmt . Errorf (
"file %q already exists in working directory. Constellation won't overwrite previous master secrets. Move it somewhere or delete it before creating a new cluster" ,
a . flags . pathPrefixer . PrefixPrintablePath ( constants . MasterSecretFilename ) ,
)
} else if ! errors . Is ( err , fs . ErrNotExist ) {
return fmt . Errorf ( "checking for %q: %w" , a . flags . pathPrefixer . PrefixPrintablePath ( constants . MasterSecretFilename ) , err )
}
return nil
}
// checkPostInitFilesExist ensures that the workspace contains the files from a previous init RPC.
func ( a * applyCmd ) checkPostInitFilesExist ( ) error {
if _ , err := a . fileHandler . Stat ( constants . AdminConfFilename ) ; err != nil {
return fmt . Errorf ( "checking for %q: %w" , a . flags . pathPrefixer . PrefixPrintablePath ( constants . AdminConfFilename ) , err )
}
if _ , err := a . fileHandler . Stat ( constants . MasterSecretFilename ) ; err != nil {
return fmt . Errorf ( "checking for %q: %w" , a . flags . pathPrefixer . PrefixPrintablePath ( constants . MasterSecretFilename ) , err )
}
return nil
}
func printCreateWarnings ( out io . Writer , conf * config . Config ) {
var printedAWarning bool
if ! conf . IsReleaseImage ( ) {
fmt . Fprintln ( out , "Configured image doesn't look like a released production image. Double check image before deploying to production." )
printedAWarning = true
}
if conf . IsNamedLikeDebugImage ( ) && ! conf . IsDebugCluster ( ) {
fmt . Fprintln ( out , "WARNING: A debug image is used but debugCluster is false." )
printedAWarning = true
}
if conf . IsDebugCluster ( ) {
fmt . Fprintln ( out , "WARNING: Creating a debug cluster. This cluster is not secure and should only be used for debugging purposes." )
fmt . Fprintln ( out , "DO NOT USE THIS CLUSTER IN PRODUCTION." )
printedAWarning = true
}
if conf . GetAttestationConfig ( ) . GetVariant ( ) . Equal ( variant . AzureTrustedLaunch { } ) {
fmt . Fprintln ( out , "Disabling Confidential VMs is insecure. Use only for evaluation purposes." )
printedAWarning = true
}
// Print an extra new line later to separate warnings from the prompt message of the create command
if printedAWarning {
fmt . Fprintln ( out , "" )
}
}
// skipPhasesCompletion returns suggestions for the skip-phases flag.
// We suggest completion for all phases that can be skipped.
// The phases may be given in any order, as a comma-separated list.
// For example, "skip-phases helm,init" should suggest all phases but "helm" and "init".
func skipPhasesCompletion ( _ * cobra . Command , _ [ ] string , toComplete string ) ( [ ] string , cobra . ShellCompDirective ) {
skippedPhases := strings . Split ( toComplete , "," )
if skippedPhases [ 0 ] == "" {
// No phases were typed yet, so suggest all phases
return allPhases ( ) , cobra . ShellCompDirectiveNoFileComp
}
// Determine what phases have already been typed by the user
phases := make ( map [ string ] struct { } )
for _ , phase := range allPhases ( ) {
phases [ phase ] = struct { } { }
}
for _ , phase := range skippedPhases {
delete ( phases , phase )
}
// Get the last phase typed by the user
// This is the phase we want to complete
lastPhase := skippedPhases [ len ( skippedPhases ) - 1 ]
fullyTypedPhases := strings . TrimSuffix ( toComplete , lastPhase )
// Add all phases that have not been typed yet to the suggestions
// The suggestion is the fully typed phases + the phase that is being completed
var suggestions [ ] string
for phase := range phases {
if strings . HasPrefix ( phase , lastPhase ) {
suggestions = append ( suggestions , fmt . Sprintf ( "%s%s" , fullyTypedPhases , phase ) )
2023-10-24 09:39:18 -04:00
}
}
2023-11-20 05:17:16 -05:00
return suggestions , cobra . ShellCompDirectiveNoFileComp
2023-10-24 09:39:18 -04:00
}
2023-12-05 06:28:40 -05:00
// warnLogger implements logging of warnings for validators.
type warnLogger struct {
cmd * cobra . Command
log debugLog
}
2024-02-08 09:20:01 -05:00
// Info messages are reduced to debug messages, since we don't want
2023-12-05 06:28:40 -05:00
// the extra info when using the CLI without setting the debug flag.
2024-02-08 09:20:01 -05:00
func ( wl warnLogger ) Info ( msg string , args ... any ) {
wl . log . Debug ( msg , args ... )
2023-12-05 06:28:40 -05:00
}
2024-02-08 09:20:01 -05:00
// Warn prints a formatted warning from the validator.
func ( wl warnLogger ) Warn ( msg string , args ... any ) {
wl . cmd . PrintErrf ( "Warning: %s %s\n" , msg , fmt . Sprint ( args ... ) )
2023-12-05 06:28:40 -05:00
}
2023-12-05 10:23:31 -05:00
2023-12-06 04:01:39 -05:00
type warnLog interface {
2024-02-08 09:20:01 -05:00
Warn ( msg string , args ... any )
Info ( msg string , args ... any )
2023-12-06 04:01:39 -05:00
}
// applier is used to run the different phases of the apply command.
type applier interface {
SetKubeConfig ( kubeConfig [ ] byte ) error
2023-12-22 04:16:36 -05:00
CheckLicense ( ctx context . Context , csp cloudprovider . Provider , initRequest bool , licenseID string ) ( int , error )
2023-12-06 04:01:39 -05:00
// methods required by "init"
GenerateMasterSecret ( ) ( uri . MasterSecret , error )
GenerateMeasurementSalt ( ) ( [ ] byte , error )
Init (
ctx context . Context , validator atls . Validator , state * state . State ,
clusterLogWriter io . Writer , payload constellation . InitPayload ,
2023-12-11 09:55:44 -05:00
) ( constellation . InitOutput , error )
2023-12-06 04:01:39 -05:00
// methods required to install/upgrade Helm charts
2024-07-03 13:37:51 -04:00
AnnotateCoreDNSResources ( context . Context ) error
2023-12-06 04:01:39 -05:00
PrepareHelmCharts (
2024-03-06 14:48:40 -05:00
flags helm . Options , state * state . State , serviceAccURI string , masterSecret uri . MasterSecret ,
2023-12-06 04:01:39 -05:00
) ( helm . Applier , bool , error )
// methods to interact with Kubernetes
ExtendClusterConfigCertSANs ( ctx context . Context , clusterEndpoint , customEndpoint string , additionalAPIServerCertSANs [ ] string ) error
GetClusterAttestationConfig ( ctx context . Context , variant variant . Variant ) ( config . AttestationCfg , error )
ApplyJoinConfig ( ctx context . Context , newAttestConfig config . AttestationCfg , measurementSalt [ ] byte ) error
UpgradeNodeImage ( ctx context . Context , imageVersion semver . Semver , imageReference string , force bool ) error
UpgradeKubernetesVersion ( ctx context . Context , kubernetesVersion versions . ValidK8sVersion , force bool ) error
BackupCRDs ( ctx context . Context , fileHandler file . Handler , upgradeDir string ) ( [ ] apiextensionsv1 . CustomResourceDefinition , error )
BackupCRs ( ctx context . Context , fileHandler file . Handler , crds [ ] apiextensionsv1 . CustomResourceDefinition , upgradeDir string ) error
}
2023-12-05 10:23:31 -05:00
// imageFetcher gets an image reference from the versionsapi.
type imageFetcher interface {
FetchReference ( ctx context . Context ,
provider cloudprovider . Provider , attestationVariant variant . Variant ,
2023-12-08 08:40:31 -05:00
image , region string , useMarketplaceImage bool ,
2023-12-05 10:23:31 -05:00
) ( string , error )
}
2024-02-21 10:39:12 -05:00
func newDebugFileLogger ( cmd * cobra . Command , fileHandler file . Handler ) ( debugLog , error ) {
logLvl := slog . LevelInfo
debugLog , err := cmd . Flags ( ) . GetBool ( "debug" )
if err != nil {
return nil , err
}
if debugLog {
logLvl = slog . LevelDebug
}
fileWriter := & fileWriter {
fileHandler : fileHandler ,
}
return slog . New (
slogmulti . Fanout (
slog . NewTextHandler ( os . Stderr , & slog . HandlerOptions { AddSource : true , Level : logLvl } ) , // first handler: stderr at log level
slog . NewJSONHandler ( fileWriter , & slog . HandlerOptions { AddSource : true , Level : slog . LevelDebug } ) , // second handler: debug JSON log to file
) ,
) , nil
}
type fileWriter struct {
fileHandler file . Handler
}
// Write satisfies the io.Writer interface by writing a message to file.
func ( l * fileWriter ) Write ( msg [ ] byte ) ( int , error ) {
err := l . fileHandler . Write ( constants . CLIDebugLogFile , msg , file . OptAppend )
return len ( msg ) , err
}