Skip to content

Commit

Permalink
Support SA Key file content JSON format in YC_SERVICE_ACCOUNT_KEY_FILE
Browse files Browse the repository at this point in the history
Closes #39
  • Loading branch information
GennadySpb authored and nywilken committed Oct 27, 2023
1 parent 22ee4bc commit c094f2f
Show file tree
Hide file tree
Showing 5 changed files with 125 additions and 12 deletions.
32 changes: 27 additions & 5 deletions builder/yandex/access_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
package yandex

import (
"encoding/json"
"errors"
"fmt"
"os"
Expand All @@ -15,6 +16,14 @@ import (
"github.com/yandex-cloud/go-sdk/iamkey"
)

type keyType int

const (
Undefined keyType = iota
File
Content
)

const (
defaultEndpoint = "api.cloud.yandex.net:443"
defaultMaxRetries = 3
Expand All @@ -24,9 +33,9 @@ const (
type AccessConfig struct {
// Non standard API endpoint. Default is `api.cloud.yandex.net:443`.
Endpoint string `mapstructure:"endpoint" required:"false"`
// Path to file with Service Account key in json format. This
// is an alternative method to authenticate to Yandex.Cloud. Alternatively you may set environment variable
// `YC_SERVICE_ACCOUNT_KEY_FILE`.
// Contains either a path to or the contents of the Service Account file in JSON format.
// This can also be specified using environment variable `YC_SERVICE_ACCOUNT_KEY_FILE`.
// You can read how to create service account key file [here](https://cloud.yandex.com/docs/iam/operations/iam-token/create-for-sa#keys-create).
ServiceAccountKeyFile string `mapstructure:"service_account_key_file" required:"false"`
// [OAuth token](https://cloud.yandex.com/docs/iam/concepts/authorization/oauth-token)
// or [IAM token](https://cloud.yandex.com/docs/iam/concepts/authorization/iam-token)
Expand All @@ -35,6 +44,8 @@ type AccessConfig struct {
Token string `mapstructure:"token" required:"true"`
// The maximum number of times an API request is being executed.
MaxRetries int `mapstructure:"max_retries"`

saKeyType keyType
}

func (c *AccessConfig) Prepare(ctx *interpolate.Context) []error {
Expand Down Expand Up @@ -66,8 +77,19 @@ func (c *AccessConfig) Prepare(ctx *interpolate.Context) []error {
}

if c.ServiceAccountKeyFile != "" {
if _, err := iamkey.ReadFromJSONFile(c.ServiceAccountKeyFile); err != nil {
errs = append(errs, fmt.Errorf("fail to read service account key file: %s", err))
// if ServiceAccountKeyFile is file path
if _, err := os.Stat(c.ServiceAccountKeyFile); err == nil {
if _, err := iamkey.ReadFromJSONFile(c.ServiceAccountKeyFile); err != nil {
errs = append(errs, fmt.Errorf("fail to read service account key file: %s", err))
}
c.saKeyType = File
} else {
// else check for a valid json data value
var f map[string]interface{}
if err := json.Unmarshal([]byte(c.ServiceAccountKeyFile), &f); err != nil {
errs = append(errs, fmt.Errorf("JSON in %q are not valid: %s", c.ServiceAccountKeyFile, err))
}
c.saKeyType = Content
}
}

Expand Down
67 changes: 67 additions & 0 deletions builder/yandex/access_config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package yandex

import (
"errors"
"os"
"testing"

"github.com/hashicorp/packer-plugin-sdk/template/interpolate"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestAccessConfig_Prepare(t *testing.T) {
bytes, err := os.ReadFile(TestServiceAccountKeyFile)
require.NoErrorf(t, err, "failed to read file %s", TestServiceAccountKeyFile)

var TestServiceAccountKeyFileContent = string(bytes)

type fields struct {
Endpoint string
ServiceAccountKeyFile string
Token string
MaxRetries int
saKeyType keyType
}
tests := []struct {
name string
fields fields
want []error
}{
{
name: "sa_key_as_file", fields: fields{
Endpoint: "",
ServiceAccountKeyFile: TestServiceAccountKeyFile,
Token: "",
},
want: nil,
},
{
name: "sa_key_as_json_content", fields: fields{
ServiceAccountKeyFile: TestServiceAccountKeyFileContent,
Token: "",
},
want: nil,
},
{
name: "both_identities", fields: fields{
ServiceAccountKeyFile: TestServiceAccountKeyFileContent,
Token: "t1.super-token",
},
want: []error{errors.New("one of token or service account key file must be specified, not both")},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := &interpolate.Context{}
c := &AccessConfig{
Endpoint: tt.fields.Endpoint,
ServiceAccountKeyFile: tt.fields.ServiceAccountKeyFile,
Token: tt.fields.Token,
MaxRetries: tt.fields.MaxRetries,
saKeyType: tt.fields.saKeyType,
}
assert.Equalf(t, tt.want, c.Prepare(ctx), "Prepare(%v)")
})
}
}
13 changes: 11 additions & 2 deletions builder/yandex/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
package yandex

import (
"io/ioutil"
"os"
"strings"
"testing"
Expand All @@ -15,9 +14,14 @@ import (
const TestServiceAccountKeyFile = "./testdata/fake-sa-key.json"

func TestConfigPrepare(t *testing.T) {
tf, err := ioutil.TempFile("", "packer")
tf, err := os.CreateTemp("", "packer")
require.NoError(t, err, "create temporary file failed")

bytes, err := os.ReadFile(TestServiceAccountKeyFile)
require.NoErrorf(t, err, "failed to read file %s", TestServiceAccountKeyFile)

var TestServiceAccountKeyFileContent = string(bytes)

defer os.Remove(tf.Name())
tf.Close()

Expand Down Expand Up @@ -47,6 +51,11 @@ func TestConfigPrepare(t *testing.T) {
TestServiceAccountKeyFile,
false,
},
{
"service_account_key_file",
TestServiceAccountKeyFileContent,
false,
},

{
"folder_id",
Expand Down
19 changes: 17 additions & 2 deletions builder/yandex/driver_yc.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package yandex

import (
"context"
"errors"
"fmt"
"log"
"strings"
Expand Down Expand Up @@ -60,8 +61,7 @@ func NewDriverYC(ui packersdk.Ui, ac *AccessConfig) (Driver, error) {
sdkConfig.Credentials = ycsdk.OAuthToken(ac.Token)
}
case ac.ServiceAccountKeyFile != "":
log.Printf("[INFO] Use Service Account key file %q for authentication", ac.ServiceAccountKeyFile)
key, err := iamkey.ReadFromJSONFile(ac.ServiceAccountKeyFile)
key, err := iamKeyFromSAKey(ac)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -106,6 +106,21 @@ func NewDriverYC(ui packersdk.Ui, ac *AccessConfig) (Driver, error) {

}

func iamKeyFromSAKey(ac *AccessConfig) (*iamkey.Key, error) {
switch ac.saKeyType {
case File:
log.Printf("[INFO] Use Service Account key (file %q) for authentication", ac.ServiceAccountKeyFile)
return iamkey.ReadFromJSONFile(ac.ServiceAccountKeyFile)
case Content:
log.Printf("[INFO] Use Service Account key (as raw JSON) for authentication")
return iamkey.ReadFromJSONBytes([]byte(ac.ServiceAccountKeyFile))
case Undefined:
return nil, errors.New("`undefined` SA key type - something goes wrong")
default:
return nil, errors.New("failed to determine SA key type; perhaps skipped the preparation stage")
}
}

func (d *driverYC) GetImage(imageID string) (*Image, error) {
image, err := d.sdk.Compute().Image().Get(context.Background(), &compute.GetImageRequest{
ImageId: imageID,
Expand Down
6 changes: 3 additions & 3 deletions docs-partials/builder/yandex/AccessConfig-not-required.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

- `endpoint` (string) - Non standard API endpoint. Default is `api.cloud.yandex.net:443`.

- `service_account_key_file` (string) - Path to file with Service Account key in json format. This
is an alternative method to authenticate to Yandex.Cloud. Alternatively you may set environment variable
`YC_SERVICE_ACCOUNT_KEY_FILE`.
- `service_account_key_file` (string) - Contains either a path to or the contents of the Service Account file in JSON format.
This can also be specified using environment variable `YC_SERVICE_ACCOUNT_KEY_FILE`.
You can read how to create service account key file [here](https://cloud.yandex.com/docs/iam/operations/iam-token/create-for-sa#keys-create).

- `max_retries` (int) - The maximum number of times an API request is being executed.

Expand Down

0 comments on commit c094f2f

Please sign in to comment.