Skip to content

Commit d8c7872

Browse files
Merge pull request #8063 from patrickdillon/create-cmd-context
OCPBUGS-32091: Add Top-level Context for Create Commands
2 parents 30ae420 + a132f85 commit d8c7872

File tree

17 files changed

+160
-63
lines changed

17 files changed

+160
-63
lines changed

cmd/openshift-install/agent.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package main
22

33
import (
4+
"context"
5+
46
"github.com/spf13/cobra"
57

68
"github.com/openshift/installer/cmd/openshift-install/agent"
@@ -14,7 +16,7 @@ import (
1416
"github.com/openshift/installer/pkg/asset/password"
1517
)
1618

17-
func newAgentCmd() *cobra.Command {
19+
func newAgentCmd(ctx context.Context) *cobra.Command {
1820
agentCmd := &cobra.Command{
1921
Use: "agent",
2022
Short: "Commands for supporting cluster installation using agent installer",
@@ -23,7 +25,7 @@ func newAgentCmd() *cobra.Command {
2325
},
2426
}
2527

26-
agentCmd.AddCommand(newAgentCreateCmd())
28+
agentCmd.AddCommand(newAgentCreateCmd(ctx))
2729
agentCmd.AddCommand(agent.NewWaitForCmd())
2830
agentCmd.AddCommand(newAgentGraphCmd())
2931
return agentCmd
@@ -115,8 +117,7 @@ var (
115117
agentTargets = []target{agentConfigTarget, agentManifestsTarget, agentImageTarget, agentPXEFilesTarget, agentConfigImageTarget, agentUnconfiguredIgnitionTarget}
116118
)
117119

118-
func newAgentCreateCmd() *cobra.Command {
119-
120+
func newAgentCreateCmd(ctx context.Context) *cobra.Command {
120121
cmd := &cobra.Command{
121122
Use: "create",
122123
Short: "Commands for generating agent installation artifacts",
@@ -127,7 +128,7 @@ func newAgentCreateCmd() *cobra.Command {
127128

128129
for _, t := range agentTargets {
129130
t.command.Args = cobra.ExactArgs(0)
130-
t.command.Run = runTargetCmd(t.assets...)
131+
t.command.Run = runTargetCmd(ctx, t.assets...)
131132
cmd.AddCommand(t.command)
132133
}
133134

cmd/openshift-install/create.go

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ const (
6868
exitCodeBootstrapFailed
6969
exitCodeInstallFailed
7070
exitCodeOperatorStabilityFailed
71+
exitCodeInterrupt
7172

7273
// coStabilityThreshold is how long a cluster operator must have Progressing=False
7374
// in order to be considered stable. Measured in seconds.
@@ -127,12 +128,10 @@ var (
127128
Short: "Create an OpenShift cluster",
128129
// FIXME: add longer descriptions for our commands with examples for better UX.
129130
// Long: "",
130-
PostRun: func(_ *cobra.Command, _ []string) {
131-
// Setup a context that is canceled when the user presses Ctrl+C,
132-
// or SIGTERM and SIGINT are received, this allows for a clean shutdown.
133-
ctx, cancel := context.WithCancel(context.TODO())
134-
defer cancel()
135-
logrus.RegisterExitHandler(cancel)
131+
PostRun: func(cmd *cobra.Command, _ []string) {
132+
133+
// Get the context that was set in newCreateCmd.
134+
ctx := cmd.Context()
136135

137136
exitCode, err := clusterCreatePostRun(ctx)
138137
if err != nil {
@@ -167,7 +166,7 @@ func clusterCreatePostRun(ctx context.Context) (int, error) {
167166
}
168167

169168
// Handle the case when the API server is not reachable.
170-
if err := handleUnreachableAPIServer(config); err != nil {
169+
if err := handleUnreachableAPIServer(ctx, config); err != nil {
171170
logrus.Fatal(fmt.Errorf("unable to handle api server override: %w", err))
172171
}
173172

@@ -176,7 +175,7 @@ func clusterCreatePostRun(ctx context.Context) (int, error) {
176175
//
177176
timer.StartTimer("Bootstrap Complete")
178177
if err := waitForBootstrapComplete(ctx, config); err != nil {
179-
bundlePath, gatherErr := runGatherBootstrapCmd(command.RootOpts.Dir)
178+
bundlePath, gatherErr := runGatherBootstrapCmd(ctx, command.RootOpts.Dir)
180179
if gatherErr != nil {
181180
logrus.Error("Attempted to gather debug logs after installation failure: ", gatherErr)
182181
}
@@ -277,7 +276,7 @@ func newClientError(errorInfo error) *clusterCreateError {
277276
}
278277
}
279278

280-
func newCreateCmd() *cobra.Command {
279+
func newCreateCmd(ctx context.Context) *cobra.Command {
281280
cmd := &cobra.Command{
282281
Use: "create",
283282
Short: "Create part of an OpenShift cluster",
@@ -288,22 +287,25 @@ func newCreateCmd() *cobra.Command {
288287

289288
for _, t := range targets {
290289
t.command.Args = cobra.ExactArgs(0)
291-
t.command.Run = runTargetCmd(t.assets...)
290+
t.command.Run = runTargetCmd(ctx, t.assets...)
292291
cmd.AddCommand(t.command)
293292
}
294293

295294
return cmd
296295
}
297296

298-
func runTargetCmd(targets ...asset.WritableAsset) func(cmd *cobra.Command, args []string) {
297+
func runTargetCmd(ctx context.Context, targets ...asset.WritableAsset) func(cmd *cobra.Command, args []string) {
299298
runner := func(directory string) error {
300299
fetcher := assetstore.NewAssetsFetcher(directory)
301-
return fetcher.FetchAndPersist(targets)
300+
return fetcher.FetchAndPersist(ctx, targets)
302301
}
303302

304303
return func(cmd *cobra.Command, args []string) {
305304
timer.StartTimer(timer.TotalTimeElapsed)
306305

306+
// Set the context to be used in the PostRun function.
307+
cmd.SetContext(ctx)
308+
307309
cleanup := command.SetupFileHook(command.RootOpts.Dir)
308310
defer cleanup()
309311

@@ -855,15 +857,15 @@ func meetsStabilityThreshold(progressing *configv1.ClusterOperatorStatusConditio
855857
return progressing.Status == configv1.ConditionFalse && time.Since(progressing.LastTransitionTime.Time).Seconds() > coStabilityThreshold
856858
}
857859

858-
func handleUnreachableAPIServer(config *rest.Config) error {
860+
func handleUnreachableAPIServer(ctx context.Context, config *rest.Config) error {
859861
assetStore, err := assetstore.NewStore(command.RootOpts.Dir)
860862
if err != nil {
861863
return fmt.Errorf("failed to create asset store: %w", err)
862864
}
863865

864866
// Ensure that the install is expecting the user to provision their own DNS solution.
865867
installConfig := &installconfig.InstallConfig{}
866-
if err := assetStore.Fetch(installConfig); err != nil {
868+
if err := assetStore.Fetch(ctx, installConfig); err != nil {
867869
return fmt.Errorf("failed to fetch %s: %w", installConfig.Name(), err)
868870
}
869871
switch installConfig.Config.Platform.Name() { //nolint:gocritic
@@ -876,7 +878,7 @@ func handleUnreachableAPIServer(config *rest.Config) error {
876878
}
877879

878880
lbConfig := &lbconfig.Config{}
879-
if err := assetStore.Fetch(lbConfig); err != nil {
881+
if err := assetStore.Fetch(ctx, lbConfig); err != nil {
880882
return fmt.Errorf("failed to fetch %s: %w", lbConfig.Name(), err)
881883
}
882884

cmd/openshift-install/gather.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ import (
3434
_ "github.com/openshift/installer/pkg/gather/gcp"
3535
)
3636

37-
func newGatherCmd() *cobra.Command {
37+
func newGatherCmd(ctx context.Context) *cobra.Command {
3838
cmd := &cobra.Command{
3939
Use: "gather",
4040
Short: "Gather debugging data for a given installation failure",
@@ -47,7 +47,7 @@ to debug the installation failures`,
4747
return cmd.Help()
4848
},
4949
}
50-
cmd.AddCommand(newGatherBootstrapCmd())
50+
cmd.AddCommand(newGatherBootstrapCmd(ctx))
5151
return cmd
5252
}
5353

@@ -58,15 +58,15 @@ var gatherBootstrapOpts struct {
5858
skipAnalysis bool
5959
}
6060

61-
func newGatherBootstrapCmd() *cobra.Command {
61+
func newGatherBootstrapCmd(ctx context.Context) *cobra.Command {
6262
cmd := &cobra.Command{
6363
Use: "bootstrap",
6464
Short: "Gather debugging data for a failing-to-bootstrap control plane",
6565
Args: cobra.ExactArgs(0),
6666
Run: func(_ *cobra.Command, _ []string) {
6767
cleanup := command.SetupFileHook(command.RootOpts.Dir)
6868
defer cleanup()
69-
bundlePath, err := runGatherBootstrapCmd(command.RootOpts.Dir)
69+
bundlePath, err := runGatherBootstrapCmd(ctx, command.RootOpts.Dir)
7070
if err != nil {
7171
logrus.Fatal(err)
7272
}
@@ -87,14 +87,14 @@ func newGatherBootstrapCmd() *cobra.Command {
8787
return cmd
8888
}
8989

90-
func runGatherBootstrapCmd(directory string) (string, error) {
90+
func runGatherBootstrapCmd(ctx context.Context, directory string) (string, error) {
9191
assetStore, err := assetstore.NewStore(directory)
9292
if err != nil {
9393
return "", errors.Wrap(err, "failed to create asset store")
9494
}
9595
// add the default bootstrap key pair to the sshKeys list
9696
bootstrapSSHKeyPair := &tls.BootstrapSSHKeyPair{}
97-
if err := assetStore.Fetch(bootstrapSSHKeyPair); err != nil {
97+
if err := assetStore.Fetch(ctx, bootstrapSSHKeyPair); err != nil {
9898
return "", errors.Wrapf(err, "failed to fetch %s", bootstrapSSHKeyPair.Name())
9999
}
100100
tmpfile, err := os.CreateTemp("", "bootstrap-ssh")
@@ -118,7 +118,7 @@ func runGatherBootstrapCmd(directory string) (string, error) {
118118

119119
if ha.Bootstrap == "" && len(ha.Masters) == 0 {
120120
config := &installconfig.InstallConfig{}
121-
if err := assetStore.Fetch(config); err != nil {
121+
if err := assetStore.Fetch(ctx, config); err != nil {
122122
return "", errors.Wrapf(err, "failed to fetch %s", config.Name())
123123
}
124124

cmd/openshift-install/main.go

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package main
22

33
import (
4+
"context"
45
"flag"
56
"io"
67
"os"
@@ -12,8 +13,10 @@ import (
1213
terminal "golang.org/x/term"
1314
"k8s.io/klog"
1415
klogv2 "k8s.io/klog/v2"
16+
"sigs.k8s.io/controller-runtime/pkg/manager/signals"
1517

1618
"github.com/openshift/installer/cmd/openshift-install/command"
19+
"github.com/openshift/installer/pkg/clusterapi"
1720
)
1821

1922
func main() {
@@ -36,18 +39,22 @@ func main() {
3639
func installerMain() {
3740
rootCmd := newRootCmd()
3841

42+
// Perform a graceful shutdown upon interrupt or at exit.
43+
ctx := handleInterrupt(signals.SetupSignalHandler())
44+
logrus.RegisterExitHandler(shutdown)
45+
3946
for _, subCmd := range []*cobra.Command{
40-
newCreateCmd(),
47+
newCreateCmd(ctx),
4148
newDestroyCmd(),
4249
newWaitForCmd(),
43-
newGatherCmd(),
50+
newGatherCmd(ctx),
4451
newAnalyzeCmd(),
4552
newVersionCmd(),
4653
newGraphCmd(),
4754
newCoreOSCmd(),
4855
newCompletionCmd(),
4956
newExplainCmd(),
50-
newAgentCmd(),
57+
newAgentCmd(ctx),
5158
} {
5259
rootCmd.AddCommand(subCmd)
5360
}
@@ -96,3 +103,26 @@ func runRootCmd(cmd *cobra.Command, args []string) {
96103
logrus.Fatal(errors.Wrap(err, "invalid log-level"))
97104
}
98105
}
106+
107+
// handleInterrupt executes a graceful shutdown then exits in
108+
// the case of a user interrupt. It returns a new context that
109+
// will be cancelled upon interrupt.
110+
func handleInterrupt(signalCtx context.Context) context.Context {
111+
ctx, cancel := context.WithCancel(context.Background())
112+
113+
// If the context from the signal handler is done,
114+
// an interrupt has been received, so shutdown & exit.
115+
go func() {
116+
<-signalCtx.Done()
117+
logrus.Warn("Received interrupt signal")
118+
shutdown()
119+
cancel()
120+
logrus.Exit(exitCodeInterrupt)
121+
}()
122+
123+
return ctx
124+
}
125+
126+
func shutdown() {
127+
clusterapi.System().Teardown()
128+
}

pkg/asset/cluster/cluster.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ type Cluster struct {
4242
}
4343

4444
var _ asset.WritableAsset = (*Cluster)(nil)
45+
var _ asset.Generator = (*Cluster)(nil)
4546

4647
// Name returns the human-friendly name of the asset.
4748
func (c *Cluster) Name() string {
@@ -78,7 +79,7 @@ func (c *Cluster) Dependencies() []asset.Asset {
7879
}
7980

8081
// Generate launches the cluster and generates the terraform state file on disk.
81-
func (c *Cluster) Generate(parents asset.Parents) (err error) {
82+
func (c *Cluster) GenerateWithContext(ctx context.Context, parents asset.Parents) (err error) {
8283
if InstallDir == "" {
8384
logrus.Fatalf("InstallDir has not been set for the %q asset", c.Name())
8485
}
@@ -136,7 +137,7 @@ func (c *Cluster) Generate(parents asset.Parents) (err error) {
136137
if err != nil {
137138
return fmt.Errorf("error getting infrastructure provider: %w", err)
138139
}
139-
files, err := provider.Provision(InstallDir, parents)
140+
files, err := provider.Provision(ctx, InstallDir, parents)
140141
if files != nil {
141142
c.FileList = append(c.FileList, files...) // append state files even in case of failure
142143
}
@@ -165,3 +166,9 @@ func (c *Cluster) Load(f asset.FileFetcher) (found bool, err error) {
165166

166167
return false, nil
167168
}
169+
170+
// Generate is implemented so the Cluster Asset maintains compatibility
171+
// with the Asset interface. It should never be called.
172+
func (c *Cluster) Generate(_ asset.Parents) (err error) {
173+
panic("Cluster.Generate was called instead of Cluster.GenerateWithContext")
174+
}

pkg/asset/generator.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package asset
2+
3+
import (
4+
"context"
5+
)
6+
7+
// Generator is used to generate assets.
8+
type Generator interface {
9+
// Generate generates this asset given
10+
// the states of its parent assets.
11+
GenerateWithContext(context.Context, Parents) error
12+
}
13+
14+
// generatorAdapter wraps an asset to provide the
15+
// Generate with context function.
16+
type generatorAdapter struct {
17+
a Asset
18+
}
19+
20+
// NewDefaultGenerator creates a new adapter to generate
21+
// an asset with a context.
22+
func NewDefaultGenerator(a Asset) Generator {
23+
return &generatorAdapter{a: a}
24+
}
25+
26+
// Generate calls Generate on an asset, dropping the context
27+
// to maintain compatibility with assets that do not implement
28+
// generate with context.
29+
func (a *generatorAdapter) GenerateWithContext(_ context.Context, p Parents) error {
30+
return a.a.Generate(p)
31+
}

pkg/asset/store.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
package asset
22

3+
import (
4+
"context"
5+
)
6+
37
// Store is a store for the states of assets.
48
type Store interface {
59
// Fetch retrieves the state of the given asset, generating it and its
610
// dependencies if necessary. When purging consumed assets, none of the
711
// assets in assetsToPreserve will be purged.
8-
Fetch(assetToFetch Asset, assetsToPreserve ...WritableAsset) error
12+
Fetch(ctx context.Context, assetToFetch Asset, assetsToPreserve ...WritableAsset) error
913

1014
// Destroy removes the asset from all its internal state and also from
1115
// disk if possible.

pkg/asset/store/assetcreate_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package store
22

33
import (
4+
"context"
45
"os"
56
"path/filepath"
67
"reflect"
@@ -100,7 +101,7 @@ func TestCreatedAssetsAreNotDirty(t *testing.T) {
100101
}
101102

102103
for _, a := range tc.targets {
103-
if err := assetStore.Fetch(a, tc.targets...); err != nil {
104+
if err := assetStore.Fetch(context.TODO(), a, tc.targets...); err != nil {
104105
t.Fatalf("failed to fetch %q: %v", a.Name(), err)
105106
}
106107

@@ -125,7 +126,7 @@ func TestCreatedAssetsAreNotDirty(t *testing.T) {
125126
for _, a := range tc.targets {
126127
name := a.Name()
127128
newAsset := reflect.New(reflect.TypeOf(a).Elem()).Interface().(asset.WritableAsset)
128-
if err := newAssetStore.Fetch(newAsset, tc.targets...); err != nil {
129+
if err := newAssetStore.Fetch(context.TODO(), newAsset, tc.targets...); err != nil {
129130
t.Fatalf("failed to fetch %q in new store: %v", a.Name(), err)
130131
}
131132
assetState := newAssetStore.assets[reflect.TypeOf(a)]

0 commit comments

Comments
 (0)