Skip to content

Commit 83e7739

Browse files
committed
refactor: use os.File for apply
1 parent 2d46390 commit 83e7739

File tree

12 files changed

+67
-71
lines changed

12 files changed

+67
-71
lines changed

apply/apply.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package apply
22

3+
import "os"
4+
35
type Cmd struct {
4-
Filename string `short:"f" predictor:"file"`
6+
Filename *os.File `short:"f" predictor:"file"`
57
FromFile fromFile `cmd:"" default:"1" name:"-f <file>" help:"Apply any resource from a yaml or json file."`
68
}

apply/file.go

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,23 +40,19 @@ func Delete() Option {
4040
}
4141
}
4242

43-
func File(ctx context.Context, client *api.Client, filename string, opts ...Option) error {
44-
if len(filename) == 0 {
43+
func File(ctx context.Context, client *api.Client, file *os.File, opts ...Option) error {
44+
if file == nil {
4545
return fmt.Errorf("missing flag -f, --filename=STRING")
4646
}
47+
defer file.Close()
4748

4849
cfg := &config{}
4950
for _, opt := range opts {
5051
opt(cfg)
5152
}
5253

53-
f, err := os.Open(filename)
54-
if err != nil {
55-
return err
56-
}
57-
5854
obj := &unstructured.Unstructured{}
59-
if err := yaml.NewYAMLOrJSONDecoder(f, 4096).Decode(obj); err != nil {
55+
if err := yaml.NewYAMLOrJSONDecoder(file, 4096).Decode(obj); err != nil {
6056
return err
6157
}
6258

apply/file_test.go

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -118,39 +118,39 @@ func TestFile(t *testing.T) {
118118
if _, err := fmt.Fprintf(f, tc.file, name, "value", runtimev1.DeletionOrphan); err != nil {
119119
t.Fatal(err)
120120
}
121+
// The file is written, but the pointer is at the end.
122+
// Close it to flush content.
123+
require.NoError(t, f.Close())
121124

122125
opts := []Option{}
123126

127+
// For delete and update tests, we first create the resource.
128+
if tc.delete || tc.update {
129+
fileToCreate, err := os.Open(f.Name())
130+
require.NoError(t, err)
131+
err = File(ctx, apiClient, fileToCreate) // This will close fileToCreate
132+
require.NoError(t, err)
133+
}
134+
124135
if tc.delete {
125-
// we need to ensure that the resource exists before we can delete it
126-
if err := File(ctx, apiClient, f.Name()); err != nil {
127-
t.Fatal(err)
128-
}
129136
opts = append(opts, Delete())
130137
}
131138

132139
if tc.update {
133-
// we need to ensure that the resource exists before we can update
134-
if err := File(ctx, apiClient, f.Name()); err != nil {
135-
t.Fatal(err)
136-
}
137-
138-
if err := f.Truncate(0); err != nil {
139-
t.Fatal(err)
140-
}
141-
142-
if _, err := f.Seek(0, 0); err != nil {
143-
t.Fatal(err)
144-
}
145-
146-
if _, err := fmt.Fprintf(f, tc.file, name, tc.updatedAnnotation, tc.updatedSpecValue); err != nil {
147-
t.Fatal(err)
148-
}
140+
// Re-create the file to truncate it and write the updated content.
141+
updatedFile, err := os.Create(f.Name())
142+
require.NoError(t, err)
143+
_, err = fmt.Fprintf(updatedFile, tc.file, name, tc.updatedAnnotation, tc.updatedSpecValue)
144+
require.NoError(t, err)
145+
require.NoError(t, updatedFile.Close())
149146

150147
opts = append(opts, UpdateOnExists())
151148
}
152149

153-
if err := File(ctx, apiClient, f.Name(), opts...); err != nil {
150+
fileToApply, err := os.Open(f.Name())
151+
require.NoError(t, err)
152+
153+
if err := File(ctx, apiClient, fileToApply, opts...); err != nil {
154154
if tc.expectedErr {
155155
return
156156
}

create/create.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"fmt"
66
"math/rand"
7+
"os"
78
"time"
89

910
runtimev1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
@@ -20,7 +21,7 @@ import (
2021
)
2122

2223
type Cmd struct {
23-
Filename string `short:"f" help:"Create any resource from a yaml or json file." predictor:"file"`
24+
Filename *os.File `short:"f" help:"Create any resource from a yaml or json file." predictor:"file"`
2425
FromFile fromFile `cmd:"" default:"1" name:"-f <file>" help:"Create any resource from a yaml or json file."`
2526
VCluster vclusterCmd `cmd:"" group:"infrastructure.nine.ch" name:"vcluster" help:"Create a new vcluster."`
2627
APIServiceAccount apiServiceAccountCmd `cmd:"" group:"iam.nine.ch" name:"apiserviceaccount" aliases:"asa" help:"Create a new API Service Account."`

create/mysql.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ type mySQLCmd struct {
2525
MachineType string `placeholder:"${mysql_machine_default}" help:"Defines the sizing for a particular MySQL instance. Available types: ${mysql_machine_types}"`
2626
AllowedCidrs []meta.IPv4CIDR `placeholder:"203.0.113.1/32" help:"Specifies the IP addresses allowed to connect to the instance." `
2727
SSHKeys []storage.SSHKey `help:"Contains a list of SSH public keys, allowed to connect to the db server, in order to up-/download and directly restore database backups."`
28-
SSHKeysFile *os.File `help:"Path to a file containing a list of SSH public keys (see above), separated by newlines. Lines prefixed with # are ignored."`
28+
SSHKeysFile *os.File `predictor:"file" help:"Path to a file containing a list of SSH public keys (see above), separated by newlines. Lines prefixed with # are ignored."`
2929
SQLMode *[]storage.MySQLMode `placeholder:"\"MODE1, MODE2, ...\"" help:"Configures the sql_mode setting. Modes affect the SQL syntax MySQL supports and the data validation checks it performs. Defaults to: ${mysql_mode}"`
3030
CharacterSetName string `placeholder:"${mysql_charset}" help:"Configures the character_set_server variable."`
3131
CharacterSetCollation string `placeholder:"${mysql_collation}" help:"Configures the collation_server variable."`

create/postgres.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ type postgresCmd struct {
2424
MachineType string `placeholder:"${postgres_machine_default}" help:"Defines the sizing for a particular PostgreSQL instance. Available types: ${postgres_machine_types}"`
2525
AllowedCidrs []meta.IPv4CIDR `placeholder:"203.0.113.1/32" help:"Specifies the IP addresses allowed to connect to the instance." `
2626
SSHKeys []storage.SSHKey `help:"Contains a list of SSH public keys, allowed to connect to the db server, in order to up-/download and directly restore database backups."`
27-
SSHKeysFile *os.File `help:"Path to a file containing a list of SSH public keys (see above), separated by newlines. Lines prefixed with # are ignored."`
27+
SSHKeysFile *os.File `predictor:"file" help:"Path to a file containing a list of SSH public keys (see above), separated by newlines. Lines prefixed with # are ignored."`
2828
PostgresVersion storage.PostgresVersion `placeholder:"${postgres_version_default}" help:"Release version with which the PostgreSQL instance is created. Available versions: ${postgres_versions}"`
2929
KeepDailyBackups *int `placeholder:"${postgres_backup_retention_days}" help:"Number of daily database backups to keep. Note that setting this to 0, backup will be disabled and existing dumps deleted immediately."`
3030
}

delete/delete.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package delete
33
import (
44
"context"
55
"fmt"
6+
"os"
67
"time"
78

89
"github.com/crossplane/crossplane-runtime/pkg/resource"
@@ -12,7 +13,7 @@ import (
1213
)
1314

1415
type Cmd struct {
15-
Filename string `short:"f" predictor:"file"`
16+
Filename *os.File `short:"f" predictor:"file"`
1617
FromFile fromFile `cmd:"" default:"1" name:"-f <file>" help:"Delete any resource from a yaml or json file."`
1718
VCluster vclusterCmd `cmd:"" group:"infrastructure.nine.ch" name:"vcluster" help:"Delete a vcluster."`
1819
APIServiceAccount apiServiceAccountCmd `cmd:"" group:"iam.nine.ch" name:"apiserviceaccount" aliases:"asa" help:"Delete an API Service Account."`

go.sum

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -579,8 +579,6 @@ github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRW
579579
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus=
580580
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
581581
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
582-
github.com/ninech/apis v0.0.0-20250919080229-7b9e4faad271 h1:EUdePYKqUvLDKRE8jGUYIwSCoYyDpuMyQIfKEMBLTmg=
583-
github.com/ninech/apis v0.0.0-20250919080229-7b9e4faad271/go.mod h1:v9N/4IvFju6G/Qp6BPanKG+V6VYsTFiytZs2hktr3Yk=
584582
github.com/ninech/apis v0.0.0-20250923145617-eda423f1b20a h1:HPjeOljDm2Nyc/Ypxu39AOgqQCQ6xqOd6sSHyUMnMyI=
585583
github.com/ninech/apis v0.0.0-20250923145617-eda423f1b20a/go.mod h1:v9N/4IvFju6G/Qp6BPanKG+V6VYsTFiytZs2hktr3Yk=
586584
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=

main.go

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -87,27 +87,10 @@ func main() {
8787
kong.BindTo(ctx, (*context.Context)(nil)),
8888
)
8989

90-
predictors := []completion.Option{
91-
completion.WithPredictor("file", complete.PredictFiles("*")),
92-
}
9390
apiClientRequired := !noAPIClientRequired(strings.Join(os.Args[1:], " "))
94-
if apiClientRequired {
95-
predictors = append(predictors,
96-
completion.WithPredictor("resource_name", predictor.NewResourceName(ctx, defaultAPICluster)),
97-
completion.WithPredictor("project_name", predictor.NewResourceNameWithKind(
98-
ctx, defaultAPICluster, management.SchemeGroupVersion.WithKind(
99-
reflect.TypeOf(management.ProjectList{}).Name(),
100-
)),
101-
),
102-
)
103-
} else {
104-
// complete needs all used predictors to be defined, so we just use
105-
// [complete.PredictNothing] for those that would require an API client.
106-
predictors = append(predictors,
107-
completion.WithPredictor("resource_name", complete.PredictNothing),
108-
completion.WithPredictor("project_name", complete.PredictNothing),
109-
)
110-
}
91+
predictors := append([]completion.Option{
92+
completion.WithPredictor("file", complete.PredictFiles("*")),
93+
}, clientPredictors(ctx, apiClientRequired)...)
11194
completion.Register(parser, predictors...)
11295

11396
kongCtx, err := parser.Parse(os.Args[1:])
@@ -149,6 +132,31 @@ func main() {
149132
}
150133
}
151134

135+
func clientPredictors(ctx context.Context, apiClientRequired bool) []completion.Option {
136+
// complete needs all used predictors to be defined, so we just use
137+
// [complete.PredictNothing] for those that would require an API client.
138+
nothing := []completion.Option{
139+
completion.WithPredictor("resource_name", complete.PredictNothing),
140+
completion.WithPredictor("project_name", complete.PredictNothing),
141+
}
142+
143+
if !apiClientRequired {
144+
return nothing
145+
}
146+
147+
client, err := predictor.NewClient(ctx, defaultAPICluster)
148+
if err != nil {
149+
return nothing
150+
}
151+
152+
return []completion.Option{
153+
completion.WithPredictor("resource_name", predictor.NewResourceName(client)),
154+
completion.WithPredictor("project_name", predictor.NewResourceNameWithKind(client,
155+
management.SchemeGroupVersion.WithKind(reflect.TypeOf(management.ProjectList{}).Name())),
156+
),
157+
}
158+
}
159+
152160
// noAPIClientRequired returns true if the command does not need to (or can't)
153161
// require an API client.
154162
func noAPIClientRequired(command string) bool {

predictor/predictor.go

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -33,21 +33,11 @@ type Resource struct {
3333
knownGVK *schema.GroupVersionKind
3434
}
3535

36-
func NewResourceName(ctx context.Context, defaultAPICluster string) complete.Predictor {
37-
// we don't want to error in our predictor so we just return an empty predictor
38-
client, err := newClient(ctx, defaultAPICluster)
39-
if err != nil {
40-
return complete.PredictNothing
41-
}
36+
func NewResourceName(client *api.Client) complete.Predictor {
4237
return &Resource{client: client}
4338
}
4439

45-
func NewResourceNameWithKind(ctx context.Context, defaultAPICluster string, gvk schema.GroupVersionKind) complete.Predictor {
46-
// we can't error in our predictor so we just return an empty predictor
47-
client, err := newClient(ctx, defaultAPICluster)
48-
if err != nil {
49-
return complete.PredictNothing
50-
}
40+
func NewResourceNameWithKind(client *api.Client, gvk schema.GroupVersionKind) complete.Predictor {
5141
return &Resource{
5242
client: client,
5343
knownGVK: ptr.To(gvk),
@@ -109,7 +99,7 @@ func listKindToResource(kind string) string {
10999
return flect.Pluralize(strings.TrimSuffix(strings.ToLower(kind), listSuffix))
110100
}
111101

112-
func newClient(ctx context.Context, defaultAPICluster string) (*api.Client, error) {
102+
func NewClient(ctx context.Context, defaultAPICluster string) (*api.Client, error) {
113103
// the client for the predictor requires a static token in the client config
114104
// since dynamic exec config seems to break with some shells during completion.
115105
// The exact reason for that is unknown.

0 commit comments

Comments
 (0)