credentialproviderpackage/pkg/configurator/bottlerocket/bottlerocket.go (211 lines of code) (raw):
package bottlerocket
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"fmt"
"io/fs"
"io/ioutil"
"net"
"net/http"
"os"
"strings"
"golang.org/x/mod/semver"
"github.com/aws/eks-anywhere-packages/credentialproviderpackage/pkg/configurator"
"github.com/aws/eks-anywhere-packages/credentialproviderpackage/pkg/constants"
)
type bottleRocket struct {
client http.Client
baseURL string
config constants.CredentialProviderConfigOptions
}
type awsCred struct {
Aws aws `json:"aws"`
}
type aws struct {
Config string `json:"config"`
Profile string `json:"profile"`
Region string `json:"region"`
}
type brKubernetes struct {
Kubernetes kubernetes `json:"kubernetes"`
}
type ecrCredentialProvider struct {
CacheDuration string `json:"cache-duration"`
Enabled bool `json:"enabled"`
ImagePatterns []string `json:"image-patterns"`
}
type credentialProviders struct {
EcrCredentialProvider ecrCredentialProvider `json:"ecr-credential-provider"`
}
type kubernetes struct {
CredentialProviders credentialProviders `json:"credential-providers"`
}
type brVersion struct {
Os struct {
Arch string `json:"arch"`
BuildID string `json:"build_id"`
PrettyName string `json:"pretty_name"`
VariantID string `json:"variant_id"`
VersionID string `json:"version_id"`
} `json:"os"`
}
var _ configurator.Configurator = (*bottleRocket)(nil)
func NewBottleRocketConfigurator(socketPath string) (*bottleRocket, error) {
socket, err := os.Stat(socketPath)
if err != nil {
return nil, err
}
if socket.Mode().Type() != fs.ModeSocket {
return nil, fmt.Errorf("Unexpected type %s expected socket\n", socket.Mode().Type())
}
br := &bottleRocket{
baseURL: "http://localhost/",
client: http.Client{
Transport: &http.Transport{
DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
return net.Dial("unix", socketPath)
},
},
},
}
valid, err := br.isSupportedBRVersion()
if err != nil {
return nil, fmt.Errorf("error retrieving BR version %v", err)
}
if !valid {
return nil, fmt.Errorf("unsupported BR version %v", err)
}
return br, nil
}
func (b *bottleRocket) Initialize(config constants.CredentialProviderConfigOptions) {
b.config = config
}
func (b *bottleRocket) UpdateAWSCredentials(path, profile string) error {
data, err := ioutil.ReadFile(path)
if err != nil {
return err
}
content := base64.StdEncoding.EncodeToString(data)
payload, err := createCredentialsPayload(content, profile)
if err != nil {
return err
}
err = b.sendSettingsSetRequest(payload)
if err != nil {
return err
}
err = b.CommitChanges()
if err != nil {
return err
}
return err
}
func (b *bottleRocket) UpdateCredentialProvider(_ string) error {
payload, err := createCredentialProviderPayload(b.config)
if err != nil {
return err
}
err = b.sendSettingsSetRequest(payload)
if err != nil {
return err
}
return err
}
func (b *bottleRocket) CommitChanges() error {
// For Bottlerocket this step is committing all changes at once
commitPath := b.baseURL + "tx/commit_and_apply"
resp, err := b.client.Post(commitPath, "application/json", bytes.NewBuffer(make([]byte, 0)))
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("failed to commit changes: %s", resp.Status)
}
return nil
}
func (b *bottleRocket) sendSettingsSetRequest(payload []byte) error {
settingsPath := b.baseURL + "settings"
req, err := http.NewRequest(http.MethodPatch, settingsPath, bytes.NewBuffer(payload))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
respPatch, err := b.client.Do(req)
if err != nil {
return err
}
defer respPatch.Body.Close()
if respPatch.StatusCode != http.StatusNoContent {
return fmt.Errorf("failed patch request: %s", respPatch.Status)
}
return nil
}
func createCredentialsPayload(content, profile string) ([]byte, error) {
aws := aws{
Config: content,
Profile: profile,
}
creds := awsCred{Aws: aws}
payload, err := json.Marshal(creds)
if err != nil {
return nil, err
}
return payload, nil
}
func createCredentialProviderPayload(config constants.CredentialProviderConfigOptions) ([]byte, error) {
providerConfig := brKubernetes{
Kubernetes: kubernetes{
credentialProviders{
ecrCredentialProvider{
Enabled: true,
ImagePatterns: config.ImagePatterns,
CacheDuration: config.DefaultCacheDuration,
},
},
},
}
payload, err := json.Marshal(providerConfig)
if err != nil {
return nil, err
}
return payload, nil
}
func (b *bottleRocket) isSupportedBRVersion() (bool, error) {
allowedVersions := "v1.11.0"
req, err := http.NewRequest(http.MethodGet, b.baseURL, nil)
if err != nil {
return false, err
}
q := req.URL.Query()
q.Add("prefix", "os")
req.URL.RawQuery = q.Encode()
respGet, err := b.client.Do(req)
if err != nil {
return false, err
}
defer respGet.Body.Close()
if respGet.StatusCode != http.StatusOK {
return false, fmt.Errorf("failed GET request: %s", respGet.Status)
}
valueBody, err := ioutil.ReadAll(respGet.Body)
if err != nil {
return false, err
}
osVersion := brVersion{}
err = json.Unmarshal(valueBody, &osVersion)
if err != nil {
return false, err
}
// Check if BR k8s version is 1.25 or greater. If so we will update allowedVersions to be >1.13.0
if osVersion.Os.VariantID != "" {
variants := strings.Split(osVersion.Os.VariantID, "-")
variantSemVar := "v" + variants[len(variants)-1]
if semver.Compare(variantSemVar, "v1.25") >= 0 {
allowedVersions = "v1.13.0"
}
}
ver := "v" + osVersion.Os.VersionID
valid := semver.Compare(ver, allowedVersions)
return valid > 0, nil
}