Skip to content

Commit ceeed36

Browse files
authored
feat(auth): add API token authentication support (#70)
Add token-based authentication as alternative to username/password. Implements mutual exclusivity validation and environment variable support. Add build tags to differentiate integration tests from unit tests. Add unit tests for configs on both post-processor and builder. Signed-off-by: Ville Vesilehto <ville.vesilehto@upcloud.com>
1 parent 76a3883 commit ceeed36

File tree

23 files changed

+851
-63
lines changed

23 files changed

+851
-63
lines changed

.web-docs/components/builder/upcloud/README.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,6 @@ The upcloud builder is used to generate storage templates on UpCloud.
99

1010
<!-- Code generated from the comments of the Config struct in builder/upcloud/config.go; DO NOT EDIT MANUALLY -->
1111

12-
- `username` (string) - The username to use when interfacing with the UpCloud API.
13-
14-
- `password` (string) - The password to use when interfacing with the UpCloud API.
15-
1612
- `zone` (string) - The zone in which the server and template should be created (e.g. nl-ams1).
1713

1814
- `storage_uuid` (string) - The UUID of the storage you want to use as a template when creating the server.
@@ -34,6 +30,12 @@ The upcloud builder is used to generate storage templates on UpCloud.
3430

3531
<!-- Code generated from the comments of the Config struct in builder/upcloud/config.go; DO NOT EDIT MANUALLY -->
3632

33+
- `username` (string) - The username to use when interfacing with the UpCloud API.
34+
35+
- `password` (string) - The password to use when interfacing with the UpCloud API.
36+
37+
- `token` (string) - The API token to use when interfacing with the UpCloud API. This is mutually exclusive with username and password.
38+
3739
- `storage_name` (string) - The name of the storage that will be used to find the first matching storage in the list of existing templates.
3840

3941
Note that `storage_uuid` parameter has higher priority. You should use either `storage_uuid` or `storage_name` for not strict matching (e.g "ubuntu server 20.04").

.web-docs/components/post-processor/upcloud-import/README.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,6 @@ Username and password configuration arguments can be omitted if environment vari
99

1010
<!-- Code generated from the comments of the Config struct in post-processor/upcloud-import/config.go; DO NOT EDIT MANUALLY -->
1111

12-
- `username` (string) - The username to use when interfacing with the UpCloud API.
13-
14-
- `password` (string) - The password to use when interfacing with the UpCloud API.
15-
1612
- `zones` ([]string) - The list of zones in which the template should be imported
1713

1814
- `template_name` (string) - The name of the template. Use `replace_existing` to replace existing template
@@ -25,6 +21,12 @@ Username and password configuration arguments can be omitted if environment vari
2521

2622
<!-- Code generated from the comments of the Config struct in post-processor/upcloud-import/config.go; DO NOT EDIT MANUALLY -->
2723

24+
- `username` (string) - The username to use when interfacing with the UpCloud API.
25+
26+
- `password` (string) - The password to use when interfacing with the UpCloud API.
27+
28+
- `token` (string) - The API token to use when interfacing with the UpCloud API. This is mutually exclusive with username and password.
29+
2830
- `replace_existing` (bool) - Replace existing template if one exists with the same name. Defaults to `false`.
2931

3032
- `storage_tier` (string) - The storage tier to use. Available options are `maxiops`, `archive`, and `standard`. Defaults to `maxiops`.

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ See updating [Changelog example here](https://keepachangelog.com/en/1.0.0/)
55

66
## [Unreleased]
77

8+
### Added
9+
10+
- Authentication token support through `UPCLOUD_TOKEN` environment variable and `token` config parameter
11+
812
## [1.6.0] - 2025-05-23
913

1014
### Added

Makefile

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,10 @@ PACKER_SDC_RENDER_DOCS=$(PACKER_SDC) renderdocs -src docs-src/ -partials docs-pa
2424
default: build
2525

2626
test:
27-
@go test -race -count $(COUNT) $(TEST) -timeout=3m
27+
@go test -race -count $(COUNT) -tags "!integration" $(TEST) -timeout=3m
2828

2929
test_integration: build install
30-
31-
PACKER_ACC=1 go test -count 1 -v $(TESTARGS) ./... -timeout=120m
30+
PACKER_ACC=1 go test -count 1 -v -tags integration $(TESTARGS) ./... -timeout=120m
3231

3332
build:
3433
@go build -v -o ${BINARY}

builder/upcloud/artifact_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
//go:build !integration
2+
13
package upcloud //nolint:testpackage // not all fields can be exported in Artifact
24

35
import (

builder/upcloud/builder.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
5454
b.driver = driver.NewDriver(&driver.DriverConfig{
5555
Username: b.config.Username,
5656
Password: b.config.Password,
57+
Token: b.config.Token,
5758
Timeout: b.config.Timeout,
5859
SSHUsername: b.config.Comm.SSHUsername,
5960
})

builder/upcloud/builder_acc_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
//go:build integration
2+
13
package upcloud //nolint:testpackage // not all fields can be exported in Artifact
24

35
import (

builder/upcloud/config.go

Lines changed: 64 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,13 @@ type Config struct {
6161
Comm communicator.Config `mapstructure:",squash"`
6262

6363
// The username to use when interfacing with the UpCloud API.
64-
Username string `mapstructure:"username" required:"true"`
64+
Username string `mapstructure:"username"`
6565

6666
// The password to use when interfacing with the UpCloud API.
67-
Password string `mapstructure:"password" required:"true"`
67+
Password string `mapstructure:"password"`
68+
69+
// The API token to use when interfacing with the UpCloud API. This is mutually exclusive with username and password.
70+
Token string `mapstructure:"token"`
6871

6972
// The zone in which the server and template should be created (e.g. nl-ams1).
7073
Zone string `mapstructure:"zone" required:"true"`
@@ -141,8 +144,8 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
141144
return nil, fmt.Errorf("failed to decode configuration: %w", err)
142145
}
143146

144-
c.setEnv()
145-
c.setDefaults()
147+
c.SetEnv()
148+
c.SetDefaults()
146149

147150
if errs := c.validate(); errs != nil && len(errs.Errors) > 0 {
148151
return nil, errs
@@ -152,7 +155,7 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
152155
}
153156

154157
// setDefaults sets default values for configuration fields.
155-
func (c *Config) setDefaults() {
158+
func (c *Config) SetDefaults() {
156159
if c.TemplatePrefix == "" && len(c.TemplateName) == 0 {
157160
c.TemplatePrefix = DefaultTemplatePrefix
158161
}
@@ -185,30 +188,73 @@ func (c *Config) validate() *packer.MultiError {
185188
errs = packer.MultiErrorAppend(errs, es...)
186189
}
187190

188-
if c.Username == "" {
191+
// Validate authentication
192+
if authErrs := c.validateAuthentication(); authErrs != nil {
193+
errs = packer.MultiErrorAppend(errs, authErrs.Errors...)
194+
}
195+
196+
// Validate required fields
197+
if c.Zone == "" {
189198
errs = packer.MultiErrorAppend(
190-
errs, errors.New("'username' must be specified"),
199+
errs, errors.New("'zone' must be specified"),
191200
)
192201
}
193202

194-
if c.Password == "" {
203+
if c.StorageUUID == "" && c.StorageName == "" {
195204
errs = packer.MultiErrorAppend(
196-
errs, errors.New("'password' must be specified"),
205+
errs, errors.New("'storage_uuid' or 'storage_name' must be specified"),
197206
)
198207
}
199208

200-
if c.Zone == "" {
209+
// Validate template configuration
210+
if templateErrs := c.validateTemplate(); templateErrs != nil {
211+
errs = packer.MultiErrorAppend(errs, templateErrs.Errors...)
212+
}
213+
214+
return errs
215+
}
216+
217+
// validateAuthentication checks authentication configuration.
218+
func (c *Config) validateAuthentication() *packer.MultiError {
219+
var errs *packer.MultiError
220+
221+
// Check authentication: either username/password OR token, but not both
222+
hasUsernamePassword := c.Username != "" || c.Password != ""
223+
hasAPIToken := c.Token != ""
224+
225+
if hasUsernamePassword && hasAPIToken {
201226
errs = packer.MultiErrorAppend(
202-
errs, errors.New("'zone' must be specified"),
227+
errs, errors.New("you cannot specify both username/password and token. Use either username/password or token for authentication"),
203228
)
204229
}
205230

206-
if c.StorageUUID == "" && c.StorageName == "" {
231+
if !hasUsernamePassword && !hasAPIToken {
207232
errs = packer.MultiErrorAppend(
208-
errs, errors.New("'storage_uuid' or 'storage_name' must be specified"),
233+
errs, errors.New("authentication required: specify either username and password, or token"),
209234
)
210235
}
211236

237+
// If using username/password, both must be provided
238+
if hasUsernamePassword && (c.Username == "" || c.Password == "") {
239+
if c.Username == "" {
240+
errs = packer.MultiErrorAppend(
241+
errs, errors.New("'username' must be specified when using username/password authentication"),
242+
)
243+
}
244+
if c.Password == "" {
245+
errs = packer.MultiErrorAppend(
246+
errs, errors.New("'password' must be specified when using username/password authentication"),
247+
)
248+
}
249+
}
250+
251+
return errs
252+
}
253+
254+
// validateTemplate checks template configuration.
255+
func (c *Config) validateTemplate() *packer.MultiError {
256+
var errs *packer.MultiError
257+
212258
if len(c.TemplatePrefix) > maxTemplatePrefixLength {
213259
errs = packer.MultiErrorAppend(
214260
errs, errors.New("'template_prefix' must be 0-40 characters"),
@@ -231,12 +277,16 @@ func (c *Config) validate() *packer.MultiError {
231277
}
232278

233279
// get params from environment.
234-
func (c *Config) setEnv() {
280+
func (c *Config) SetEnv() {
235281
if c.Username == "" {
236282
c.Username = driver.UsernameFromEnv()
237283
}
238284

239285
if c.Password == "" {
240286
c.Password = driver.PasswordFromEnv()
241287
}
288+
289+
if c.Token == "" {
290+
c.Token = driver.TokenFromEnv()
291+
}
242292
}

builder/upcloud/config.hcl2spec.go

Lines changed: 4 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)