mirror of
https://github.com/zalando/postgres-operator.git
synced 2026-10-01 09:07:17 +02:00
ebs_tags_from_labels to add tags to EBS Volumes
This commit is contained in:
@@ -63,6 +63,10 @@ spec:
|
||||
aws_region:
|
||||
default: eu-central-1
|
||||
type: string
|
||||
ebs_tags_inherit_labels:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
enable_ebs_gp3_migration:
|
||||
type: boolean
|
||||
enable_ebs_gp3_migration_max_size:
|
||||
|
||||
@@ -41,6 +41,7 @@ data:
|
||||
enable_cross_namespace_secret: "false"
|
||||
enable_finalizers: "false"
|
||||
enable_database_access: "true"
|
||||
# ebs_tags_inherit_labels: "application,team"
|
||||
enable_ebs_gp3_migration: "false"
|
||||
enable_ebs_gp3_migration_max_size: "1000"
|
||||
enable_init_containers: "true"
|
||||
|
||||
@@ -64,6 +64,10 @@ spec:
|
||||
aws_region:
|
||||
default: eu-central-1
|
||||
type: string
|
||||
ebs_tags_inherit_labels:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
enable_ebs_gp3_migration:
|
||||
type: boolean
|
||||
enable_ebs_gp3_migration_max_size:
|
||||
|
||||
@@ -167,6 +167,9 @@ configuration:
|
||||
# additional_secret_mount: "some-secret-name"
|
||||
# additional_secret_mount_path: "/some/dir"
|
||||
aws_region: eu-central-1
|
||||
# ebs_tags_inherit_labels:
|
||||
# - application
|
||||
# - team
|
||||
enable_ebs_gp3_migration: false
|
||||
# enable_ebs_gp3_migration_max_size: 1000
|
||||
# gcp_credentials: ""
|
||||
|
||||
@@ -247,16 +247,17 @@ type LoadBalancerConfiguration struct {
|
||||
type AWSGCPConfiguration struct {
|
||||
WALES3Bucket string `json:"wal_s3_bucket,omitempty"`
|
||||
// +kubebuilder:default=eu-central-1
|
||||
AWSRegion string `json:"aws_region,omitempty"`
|
||||
WALGSBucket string `json:"wal_gs_bucket,omitempty"`
|
||||
GCPCredentials string `json:"gcp_credentials,omitempty"`
|
||||
WALAZStorageAccount string `json:"wal_az_storage_account,omitempty"`
|
||||
LogS3Bucket string `json:"log_s3_bucket,omitempty"`
|
||||
KubeIAMRole string `json:"kube_iam_role,omitempty"`
|
||||
AdditionalSecretMount string `json:"additional_secret_mount,omitempty"`
|
||||
AdditionalSecretMountPath string `json:"additional_secret_mount_path,omitempty"`
|
||||
EnableEBSGp3Migration bool `json:"enable_ebs_gp3_migration,omitempty"`
|
||||
EnableEBSGp3MigrationMaxSize int64 `json:"enable_ebs_gp3_migration_max_size,omitempty"`
|
||||
AWSRegion string `json:"aws_region,omitempty"`
|
||||
WALGSBucket string `json:"wal_gs_bucket,omitempty"`
|
||||
GCPCredentials string `json:"gcp_credentials,omitempty"`
|
||||
WALAZStorageAccount string `json:"wal_az_storage_account,omitempty"`
|
||||
LogS3Bucket string `json:"log_s3_bucket,omitempty"`
|
||||
KubeIAMRole string `json:"kube_iam_role,omitempty"`
|
||||
AdditionalSecretMount string `json:"additional_secret_mount,omitempty"`
|
||||
AdditionalSecretMountPath string `json:"additional_secret_mount_path,omitempty"`
|
||||
EnableEBSGp3Migration bool `json:"enable_ebs_gp3_migration,omitempty"`
|
||||
EnableEBSGp3MigrationMaxSize int64 `json:"enable_ebs_gp3_migration_max_size,omitempty"`
|
||||
EBSTagsInheritLabels []string `json:"ebs_tags_inherit_labels,omitempty"`
|
||||
}
|
||||
|
||||
// OperatorDebugConfiguration defines options for the debug mode
|
||||
|
||||
@@ -64,6 +64,10 @@ spec:
|
||||
aws_region:
|
||||
default: eu-central-1
|
||||
type: string
|
||||
ebs_tags_inherit_labels:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
enable_ebs_gp3_migration:
|
||||
type: boolean
|
||||
enable_ebs_gp3_migration_max_size:
|
||||
|
||||
@@ -37,6 +37,11 @@ import (
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *AWSGCPConfiguration) DeepCopyInto(out *AWSGCPConfiguration) {
|
||||
*out = *in
|
||||
if in.EBSTagsInheritLabels != nil {
|
||||
in, out := &in.EBSTagsInheritLabels, &out.EBSTagsInheritLabels
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -470,7 +475,7 @@ func (in *OperatorConfigurationData) DeepCopyInto(out *OperatorConfigurationData
|
||||
out.PostgresPodResources = in.PostgresPodResources
|
||||
out.Timeouts = in.Timeouts
|
||||
in.LoadBalancer.DeepCopyInto(&out.LoadBalancer)
|
||||
out.AWSGCP = in.AWSGCP
|
||||
in.AWSGCP.DeepCopyInto(&out.AWSGCP)
|
||||
in.OperatorDebug.DeepCopyInto(&out.OperatorDebug)
|
||||
in.TeamsAPI.DeepCopyInto(&out.TeamsAPI)
|
||||
out.LoggingRESTAPI = in.LoggingRESTAPI
|
||||
|
||||
@@ -37,6 +37,9 @@ func (c *Cluster) syncVolumes() error {
|
||||
if err != nil {
|
||||
c.logger.Errorf("populating EBS meta data failed, skipping potential adjustments: %v", err)
|
||||
} else {
|
||||
if err = c.tagEBSVolumes(); err != nil {
|
||||
c.logger.Warningf("tagging EBS volumes failed: %v", err)
|
||||
}
|
||||
err = c.syncUnderlyingEBSVolume()
|
||||
if err != nil {
|
||||
c.logger.Errorf("errors occurred during EBS volume adjustments: %v", err)
|
||||
@@ -56,6 +59,11 @@ func (c *Cluster) syncVolumes() error {
|
||||
// TODO: handle the case of the cluster that is downsized and enlarged again
|
||||
// (there will be a volume from the old pod for which we can't act before the
|
||||
// the statefulset modification is concluded)
|
||||
if err = c.populateVolumeMetaData(); err != nil {
|
||||
c.logger.Warningf("populating EBS meta data failed, skipping EBS volume tagging: %v", err)
|
||||
} else if err = c.tagEBSVolumes(); err != nil {
|
||||
c.logger.Warningf("tagging EBS volumes failed: %v", err)
|
||||
}
|
||||
if err = c.syncEbsVolumes(); err != nil {
|
||||
err = fmt.Errorf("could not sync persistent volumes: %v", err)
|
||||
return err
|
||||
@@ -497,3 +505,75 @@ func (c *Cluster) executeEBSMigration() error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// tagEBSVolumes propagates labels (by name) from the PostgreSQL CR to EBS volumes as tags.
|
||||
func (c *Cluster) tagEBSVolumes() error {
|
||||
if c.VolumeResizer == nil {
|
||||
return fmt.Errorf("no volume resizer set for EBS volume tagging")
|
||||
}
|
||||
|
||||
if len(c.OpConfig.EBSTagsInheritLabels) == 0 {
|
||||
c.logger.Debugf("no label keys configured for EBS tag inheritance, skipping")
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(c.EBSVolumes) == 0 {
|
||||
c.logger.Debugf("no EBS volumes found for tagging")
|
||||
return nil
|
||||
}
|
||||
|
||||
desiredTags := make(map[string]string)
|
||||
for _, labelKey := range c.OpConfig.EBSTagsInheritLabels {
|
||||
labelValue, ok := c.ObjectMeta.Labels[labelKey]
|
||||
if !ok || labelValue == "" {
|
||||
c.logger.Debugf("label %q not found or empty on CR, skipping", labelKey)
|
||||
continue
|
||||
}
|
||||
desiredTags[labelKey] = labelValue
|
||||
}
|
||||
|
||||
if len(desiredTags) == 0 {
|
||||
c.logger.Debugf("no tags to apply from configured labels")
|
||||
return nil
|
||||
}
|
||||
|
||||
volumesToTag := make([]string, 0, len(c.EBSVolumes))
|
||||
for volumeID, volumeProps := range c.EBSVolumes {
|
||||
if c.tagsNeedUpdate(volumeProps.Tags, desiredTags) {
|
||||
volumesToTag = append(volumesToTag, volumeID)
|
||||
}
|
||||
}
|
||||
|
||||
if len(volumesToTag) == 0 {
|
||||
c.logger.Debugf("all EBS volumes already have the desired tags")
|
||||
return nil
|
||||
}
|
||||
|
||||
if !c.VolumeResizer.IsConnectedToProvider() {
|
||||
if err := c.VolumeResizer.ConnectToProvider(); err != nil {
|
||||
return fmt.Errorf("could not connect to volume provider for tagging: %v", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := c.VolumeResizer.DisconnectFromProvider(); err != nil {
|
||||
c.logger.Errorf("disconnecting from volume provider failed: %v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
if err := c.VolumeResizer.TagVolumes(volumesToTag, desiredTags); err != nil {
|
||||
return fmt.Errorf("could not tag EBS volumes: %v", err)
|
||||
}
|
||||
|
||||
c.logger.Infof("successfully tagged %d EBS volumes with labels: %v", len(volumesToTag), desiredTags)
|
||||
return nil
|
||||
}
|
||||
|
||||
// tagsNeedUpdate returns true if any desired tag is missing or has a different value.
|
||||
func (c *Cluster) tagsNeedUpdate(existingTags, desiredTags map[string]string) bool {
|
||||
for key, desiredValue := range desiredTags {
|
||||
if existingTags[key] != desiredValue {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -193,6 +193,7 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur
|
||||
result.AdditionalSecretMountPath = fromCRD.AWSGCP.AdditionalSecretMountPath
|
||||
result.EnableEBSGp3Migration = fromCRD.AWSGCP.EnableEBSGp3Migration
|
||||
result.EnableEBSGp3MigrationMaxSize = util.CoalesceInt64(fromCRD.AWSGCP.EnableEBSGp3MigrationMaxSize, 1000)
|
||||
result.EBSTagsInheritLabels = fromCRD.AWSGCP.EBSTagsInheritLabels
|
||||
|
||||
// logical backup config
|
||||
result.LogicalBackupSchedule = util.Coalesce(fromCRD.LogicalBackup.Schedule, "30 00 * * *")
|
||||
|
||||
@@ -201,6 +201,7 @@ type Config struct {
|
||||
AdditionalSecretMountPath string `name:"additional_secret_mount_path"`
|
||||
EnableEBSGp3Migration bool `name:"enable_ebs_gp3_migration" default:"false"`
|
||||
EnableEBSGp3MigrationMaxSize int64 `name:"enable_ebs_gp3_migration_max_size" default:"1000"`
|
||||
EBSTagsInheritLabels []string `name:"ebs_tags_inherit_labels" default:""`
|
||||
DebugLogging bool `name:"debug_logging" default:"true"`
|
||||
EnableDBAccess bool `name:"enable_database_access" default:"true"`
|
||||
EnableTeamsAPI bool `name:"enable_teams_api" default:"true"`
|
||||
|
||||
@@ -17,7 +17,7 @@ const (
|
||||
resize2fs = "resize2fs"
|
||||
)
|
||||
|
||||
//Ext234Resize implements the FilesystemResizer interface for the ext4/3/2fs.
|
||||
// Ext234Resize implements the FilesystemResizer interface for the ext4/3/2fs.
|
||||
type Ext234Resize struct {
|
||||
}
|
||||
|
||||
|
||||
+243
-243
@@ -1,243 +1,243 @@
|
||||
package teams
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var (
|
||||
logger = logrus.New().WithField("pkg", "teamsapi")
|
||||
token = "ec45b1cfbe7100c6315d183a3eb6cec0M2U1LWJkMzEtZDgzNzNmZGQyNGM3IiwiYXV0aF90aW1lIjoxNDkzNzMwNzQ1LCJpc3MiOiJodHRwcz"
|
||||
input = `{
|
||||
"dn": "cn=100100,ou=official,ou=foobar,dc=zalando,dc=net",
|
||||
"id": "acid",
|
||||
"id_name": "acid",
|
||||
"team_id": "111222",
|
||||
"type": "official",
|
||||
"name": "Acid team name",
|
||||
"mail": [
|
||||
"email1@example.com",
|
||||
"email2@example.com"
|
||||
],
|
||||
"alias": [
|
||||
"acid"
|
||||
],
|
||||
"member": [
|
||||
"member1",
|
||||
"member2",
|
||||
"member3"
|
||||
],
|
||||
"infrastructure-accounts": [
|
||||
{
|
||||
"id": "1234512345",
|
||||
"name": "acid",
|
||||
"provider": "aws",
|
||||
"type": "aws",
|
||||
"description": "",
|
||||
"owner": "acid",
|
||||
"owner_dn": "cn=100100,ou=official,ou=foobar,dc=zalando,dc=net",
|
||||
"disabled": false
|
||||
},
|
||||
{
|
||||
"id": "5432154321",
|
||||
"name": "db",
|
||||
"provider": "aws",
|
||||
"type": "aws",
|
||||
"description": "",
|
||||
"owner": "acid",
|
||||
"owner_dn": "cn=100100,ou=official,ou=foobar,dc=zalando,dc=net",
|
||||
"disabled": false
|
||||
}
|
||||
],
|
||||
"cost_center": "00099999",
|
||||
"delivery_lead": "member4",
|
||||
"parent_team_id": "111221"
|
||||
}`
|
||||
)
|
||||
var teamsAPItc = []struct {
|
||||
in string
|
||||
inCode int
|
||||
inTeam string
|
||||
out *Team
|
||||
err error
|
||||
}{
|
||||
{
|
||||
input,
|
||||
200,
|
||||
"acid",
|
||||
&Team{
|
||||
Dn: "cn=100100,ou=official,ou=foobar,dc=zalando,dc=net",
|
||||
ID: "acid",
|
||||
TeamName: "acid",
|
||||
TeamID: "111222",
|
||||
Type: "official",
|
||||
FullName: "Acid team name",
|
||||
Aliases: []string{"acid"},
|
||||
Mails: []string{"email1@example.com", "email2@example.com"},
|
||||
Members: []string{"member1", "member2", "member3"},
|
||||
CostCenter: "00099999",
|
||||
DeliveryLead: "member4",
|
||||
ParentTeamID: "111221",
|
||||
InfrastructureAccounts: []infrastructureAccount{
|
||||
{
|
||||
ID: "1234512345",
|
||||
Name: "acid",
|
||||
Provider: "aws",
|
||||
Type: "aws",
|
||||
Description: "",
|
||||
Owner: "acid",
|
||||
OwnerDn: "cn=100100,ou=official,ou=foobar,dc=zalando,dc=net",
|
||||
Disabled: false},
|
||||
{
|
||||
ID: "5432154321",
|
||||
Name: "db",
|
||||
Provider: "aws",
|
||||
Type: "aws",
|
||||
Description: "",
|
||||
Owner: "acid",
|
||||
OwnerDn: "cn=100100,ou=official,ou=foobar,dc=zalando,dc=net",
|
||||
Disabled: false},
|
||||
},
|
||||
},
|
||||
nil}, {
|
||||
`{"error": "Access Token not valid"}`,
|
||||
401,
|
||||
"acid",
|
||||
nil,
|
||||
fmt.Errorf(`team API query failed with status code 401 and message: '"Access Token not valid"'`),
|
||||
},
|
||||
{
|
||||
`{"status": "I'm a teapot'"}`,
|
||||
418,
|
||||
"acid",
|
||||
nil,
|
||||
fmt.Errorf(`team API query failed with status code 418`),
|
||||
},
|
||||
{
|
||||
`{"status": "I'm a teapot`,
|
||||
418,
|
||||
"acid",
|
||||
nil,
|
||||
fmt.Errorf(`team API query failed with status code 418 and malformed response: unexpected EOF`),
|
||||
},
|
||||
{
|
||||
`{"status": "I'm a teapot`,
|
||||
200,
|
||||
"acid",
|
||||
nil,
|
||||
fmt.Errorf(`could not parse team API response: unexpected EOF`),
|
||||
},
|
||||
{
|
||||
input,
|
||||
404,
|
||||
"banana",
|
||||
nil,
|
||||
fmt.Errorf(`team API query failed with status code 404`),
|
||||
},
|
||||
}
|
||||
|
||||
var requestsURLtc = []struct {
|
||||
url string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
"coffee://localhost/",
|
||||
fmt.Errorf(`Get "coffee://localhost/teams/acid": unsupported protocol scheme "coffee"`),
|
||||
},
|
||||
{
|
||||
"http://192.168.0.%31/",
|
||||
fmt.Errorf(`parse "http://192.168.0.%%31/teams/acid": invalid URL escape "%%31"`),
|
||||
},
|
||||
}
|
||||
|
||||
func TestInfo(t *testing.T) {
|
||||
for _, tc := range teamsAPItc {
|
||||
func() {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Header.Get("Authorization") != "Bearer "+token {
|
||||
t.Errorf("authorization token is wrong or not provided")
|
||||
}
|
||||
w.WriteHeader(tc.inCode)
|
||||
if _, err := fmt.Fprint(w, tc.in); err != nil {
|
||||
t.Errorf("error writing teams api response %v", err)
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
api := NewTeamsAPI(ts.URL, logger)
|
||||
|
||||
actual, statusCode, err := api.TeamInfo(tc.inTeam, token)
|
||||
if err != nil && err.Error() != tc.err.Error() {
|
||||
t.Errorf("expected error: %v, got: %v", tc.err, err)
|
||||
return
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(actual, tc.out) {
|
||||
t.Errorf("expected %#v, got: %#v", tc.out, actual)
|
||||
}
|
||||
|
||||
if statusCode != tc.inCode {
|
||||
t.Errorf("expected %d, got: %d", tc.inCode, statusCode)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
type mockHTTPClient struct {
|
||||
}
|
||||
|
||||
type mockBody struct {
|
||||
}
|
||||
|
||||
func (b *mockBody) Read(p []byte) (n int, err error) {
|
||||
return 2, nil
|
||||
}
|
||||
|
||||
func (b *mockBody) Close() error {
|
||||
return fmt.Errorf("close error")
|
||||
}
|
||||
|
||||
func (c *mockHTTPClient) Do(req *http.Request) (*http.Response, error) {
|
||||
resp := http.Response{
|
||||
Status: "200 OK",
|
||||
StatusCode: 200,
|
||||
ContentLength: 2,
|
||||
Close: false,
|
||||
Request: req,
|
||||
}
|
||||
resp.Body = &mockBody{}
|
||||
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
func TestHttpClientClose(t *testing.T) {
|
||||
ts := httptest.NewServer(nil)
|
||||
|
||||
api := NewTeamsAPI(ts.URL, logger)
|
||||
api.httpClient = &mockHTTPClient{}
|
||||
|
||||
_, _, err := api.TeamInfo("acid", token)
|
||||
expError := fmt.Errorf("error when closing response: close error")
|
||||
if err.Error() != expError.Error() {
|
||||
t.Errorf("expected error: %v, got: %v", expError, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequest(t *testing.T) {
|
||||
for _, tc := range requestsURLtc {
|
||||
api := NewTeamsAPI(tc.url, logger)
|
||||
resp, _, err := api.TeamInfo("acid", token)
|
||||
if resp != nil {
|
||||
t.Errorf("response expected to be nil")
|
||||
continue
|
||||
}
|
||||
|
||||
if err.Error() != tc.err.Error() {
|
||||
t.Errorf("expected error: %v, got: %v", tc.err, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
package teams
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var (
|
||||
logger = logrus.New().WithField("pkg", "teamsapi")
|
||||
token = "ec45b1cfbe7100c6315d183a3eb6cec0M2U1LWJkMzEtZDgzNzNmZGQyNGM3IiwiYXV0aF90aW1lIjoxNDkzNzMwNzQ1LCJpc3MiOiJodHRwcz"
|
||||
input = `{
|
||||
"dn": "cn=100100,ou=official,ou=foobar,dc=zalando,dc=net",
|
||||
"id": "acid",
|
||||
"id_name": "acid",
|
||||
"team_id": "111222",
|
||||
"type": "official",
|
||||
"name": "Acid team name",
|
||||
"mail": [
|
||||
"email1@example.com",
|
||||
"email2@example.com"
|
||||
],
|
||||
"alias": [
|
||||
"acid"
|
||||
],
|
||||
"member": [
|
||||
"member1",
|
||||
"member2",
|
||||
"member3"
|
||||
],
|
||||
"infrastructure-accounts": [
|
||||
{
|
||||
"id": "1234512345",
|
||||
"name": "acid",
|
||||
"provider": "aws",
|
||||
"type": "aws",
|
||||
"description": "",
|
||||
"owner": "acid",
|
||||
"owner_dn": "cn=100100,ou=official,ou=foobar,dc=zalando,dc=net",
|
||||
"disabled": false
|
||||
},
|
||||
{
|
||||
"id": "5432154321",
|
||||
"name": "db",
|
||||
"provider": "aws",
|
||||
"type": "aws",
|
||||
"description": "",
|
||||
"owner": "acid",
|
||||
"owner_dn": "cn=100100,ou=official,ou=foobar,dc=zalando,dc=net",
|
||||
"disabled": false
|
||||
}
|
||||
],
|
||||
"cost_center": "00099999",
|
||||
"delivery_lead": "member4",
|
||||
"parent_team_id": "111221"
|
||||
}`
|
||||
)
|
||||
var teamsAPItc = []struct {
|
||||
in string
|
||||
inCode int
|
||||
inTeam string
|
||||
out *Team
|
||||
err error
|
||||
}{
|
||||
{
|
||||
input,
|
||||
200,
|
||||
"acid",
|
||||
&Team{
|
||||
Dn: "cn=100100,ou=official,ou=foobar,dc=zalando,dc=net",
|
||||
ID: "acid",
|
||||
TeamName: "acid",
|
||||
TeamID: "111222",
|
||||
Type: "official",
|
||||
FullName: "Acid team name",
|
||||
Aliases: []string{"acid"},
|
||||
Mails: []string{"email1@example.com", "email2@example.com"},
|
||||
Members: []string{"member1", "member2", "member3"},
|
||||
CostCenter: "00099999",
|
||||
DeliveryLead: "member4",
|
||||
ParentTeamID: "111221",
|
||||
InfrastructureAccounts: []infrastructureAccount{
|
||||
{
|
||||
ID: "1234512345",
|
||||
Name: "acid",
|
||||
Provider: "aws",
|
||||
Type: "aws",
|
||||
Description: "",
|
||||
Owner: "acid",
|
||||
OwnerDn: "cn=100100,ou=official,ou=foobar,dc=zalando,dc=net",
|
||||
Disabled: false},
|
||||
{
|
||||
ID: "5432154321",
|
||||
Name: "db",
|
||||
Provider: "aws",
|
||||
Type: "aws",
|
||||
Description: "",
|
||||
Owner: "acid",
|
||||
OwnerDn: "cn=100100,ou=official,ou=foobar,dc=zalando,dc=net",
|
||||
Disabled: false},
|
||||
},
|
||||
},
|
||||
nil}, {
|
||||
`{"error": "Access Token not valid"}`,
|
||||
401,
|
||||
"acid",
|
||||
nil,
|
||||
fmt.Errorf(`team API query failed with status code 401 and message: '"Access Token not valid"'`),
|
||||
},
|
||||
{
|
||||
`{"status": "I'm a teapot'"}`,
|
||||
418,
|
||||
"acid",
|
||||
nil,
|
||||
fmt.Errorf(`team API query failed with status code 418`),
|
||||
},
|
||||
{
|
||||
`{"status": "I'm a teapot`,
|
||||
418,
|
||||
"acid",
|
||||
nil,
|
||||
fmt.Errorf(`team API query failed with status code 418 and malformed response: unexpected EOF`),
|
||||
},
|
||||
{
|
||||
`{"status": "I'm a teapot`,
|
||||
200,
|
||||
"acid",
|
||||
nil,
|
||||
fmt.Errorf(`could not parse team API response: unexpected EOF`),
|
||||
},
|
||||
{
|
||||
input,
|
||||
404,
|
||||
"banana",
|
||||
nil,
|
||||
fmt.Errorf(`team API query failed with status code 404`),
|
||||
},
|
||||
}
|
||||
|
||||
var requestsURLtc = []struct {
|
||||
url string
|
||||
err error
|
||||
}{
|
||||
{
|
||||
"coffee://localhost/",
|
||||
fmt.Errorf(`Get "coffee://localhost/teams/acid": unsupported protocol scheme "coffee"`),
|
||||
},
|
||||
{
|
||||
"http://192.168.0.%31/",
|
||||
fmt.Errorf(`parse "http://192.168.0.%%31/teams/acid": invalid URL escape "%%31"`),
|
||||
},
|
||||
}
|
||||
|
||||
func TestInfo(t *testing.T) {
|
||||
for _, tc := range teamsAPItc {
|
||||
func() {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Header.Get("Authorization") != "Bearer "+token {
|
||||
t.Errorf("authorization token is wrong or not provided")
|
||||
}
|
||||
w.WriteHeader(tc.inCode)
|
||||
if _, err := fmt.Fprint(w, tc.in); err != nil {
|
||||
t.Errorf("error writing teams api response %v", err)
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
api := NewTeamsAPI(ts.URL, logger)
|
||||
|
||||
actual, statusCode, err := api.TeamInfo(tc.inTeam, token)
|
||||
if err != nil && err.Error() != tc.err.Error() {
|
||||
t.Errorf("expected error: %v, got: %v", tc.err, err)
|
||||
return
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(actual, tc.out) {
|
||||
t.Errorf("expected %#v, got: %#v", tc.out, actual)
|
||||
}
|
||||
|
||||
if statusCode != tc.inCode {
|
||||
t.Errorf("expected %d, got: %d", tc.inCode, statusCode)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
type mockHTTPClient struct {
|
||||
}
|
||||
|
||||
type mockBody struct {
|
||||
}
|
||||
|
||||
func (b *mockBody) Read(p []byte) (n int, err error) {
|
||||
return 2, nil
|
||||
}
|
||||
|
||||
func (b *mockBody) Close() error {
|
||||
return fmt.Errorf("close error")
|
||||
}
|
||||
|
||||
func (c *mockHTTPClient) Do(req *http.Request) (*http.Response, error) {
|
||||
resp := http.Response{
|
||||
Status: "200 OK",
|
||||
StatusCode: 200,
|
||||
ContentLength: 2,
|
||||
Close: false,
|
||||
Request: req,
|
||||
}
|
||||
resp.Body = &mockBody{}
|
||||
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
func TestHttpClientClose(t *testing.T) {
|
||||
ts := httptest.NewServer(nil)
|
||||
|
||||
api := NewTeamsAPI(ts.URL, logger)
|
||||
api.httpClient = &mockHTTPClient{}
|
||||
|
||||
_, _, err := api.TeamInfo("acid", token)
|
||||
expError := fmt.Errorf("error when closing response: close error")
|
||||
if err.Error() != expError.Error() {
|
||||
t.Errorf("expected error: %v, got: %v", expError, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequest(t *testing.T) {
|
||||
for _, tc := range requestsURLtc {
|
||||
api := NewTeamsAPI(tc.url, logger)
|
||||
resp, _, err := api.TeamInfo("acid", token)
|
||||
if resp != nil {
|
||||
t.Errorf("response expected to be nil")
|
||||
continue
|
||||
}
|
||||
|
||||
if err.Error() != tc.err.Error() {
|
||||
t.Errorf("expected error: %v, got: %v", tc.err, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+37
-2
@@ -89,11 +89,18 @@ func (r *EBSVolumeResizer) DescribeVolumes(volumeIds []string) ([]VolumeProperti
|
||||
}
|
||||
|
||||
for _, v := range volumeOutput.Volumes {
|
||||
tags := make(map[string]string)
|
||||
for _, tag := range v.Tags {
|
||||
if tag.Key != nil && tag.Value != nil {
|
||||
tags[*tag.Key] = *tag.Value
|
||||
}
|
||||
}
|
||||
|
||||
switch v.VolumeType {
|
||||
case "gp3":
|
||||
p = append(p, VolumeProperties{VolumeID: *v.VolumeId, Size: int64(*v.Size), VolumeType: string(v.VolumeType), Iops: int64(*v.Iops), Throughput: int64(*v.Throughput)})
|
||||
p = append(p, VolumeProperties{VolumeID: *v.VolumeId, Size: int64(*v.Size), VolumeType: string(v.VolumeType), Iops: int64(*v.Iops), Throughput: int64(*v.Throughput), Tags: tags})
|
||||
case "gp2":
|
||||
p = append(p, VolumeProperties{VolumeID: *v.VolumeId, Size: int64(*v.Size), VolumeType: string(v.VolumeType)})
|
||||
p = append(p, VolumeProperties{VolumeID: *v.VolumeId, Size: int64(*v.Size), VolumeType: string(v.VolumeType), Tags: tags})
|
||||
default:
|
||||
return nil, fmt.Errorf("discovered unexpected volume type %s %s", *v.VolumeId, v.VolumeType)
|
||||
}
|
||||
@@ -206,6 +213,34 @@ func (r *EBSVolumeResizer) ModifyVolume(volumeID string, newType *string, newSiz
|
||||
})
|
||||
}
|
||||
|
||||
// TagVolumes tags the given EBS volumes with the provided tags.
|
||||
func (r *EBSVolumeResizer) TagVolumes(volumeIds []string, tags map[string]string) error {
|
||||
if !r.IsConnectedToProvider() {
|
||||
if err := r.ConnectToProvider(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(volumeIds) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
ec2Tags := make([]types.Tag, 0, len(tags))
|
||||
for key, value := range tags {
|
||||
k, v := key, value
|
||||
ec2Tags = append(ec2Tags, types.Tag{Key: &k, Value: &v})
|
||||
}
|
||||
|
||||
_, err := r.connection.CreateTags(context.TODO(), &ec2.CreateTagsInput{
|
||||
Resources: volumeIds,
|
||||
Tags: ec2Tags,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not tag EBS volumes: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DisconnectFromProvider closes connection to the EC2 instance
|
||||
func (r *EBSVolumeResizer) DisconnectFromProvider() error {
|
||||
r.connection = nil
|
||||
|
||||
@@ -2,9 +2,9 @@ package volumes
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetProviderVolumeID(t *testing.T) {
|
||||
@@ -88,7 +88,7 @@ func TestVolumeBelongsToProvider(t *testing.T) {
|
||||
name: "AWS EBS volume handle",
|
||||
pv: &v1.PersistentVolume{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string {
|
||||
Annotations: map[string]string{
|
||||
"pv.kubernetes.io/provisioned-by": "kubernetes.io/aws-ebs",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -11,6 +11,7 @@ type VolumeProperties struct {
|
||||
Size int64
|
||||
Iops int64
|
||||
Throughput int64
|
||||
Tags map[string]string
|
||||
}
|
||||
|
||||
// VolumeResizer defines the set of methods used to implememnt provider-specific resizing of persistent volumes.
|
||||
@@ -24,4 +25,5 @@ type VolumeResizer interface {
|
||||
ModifyVolume(providerVolumeID string, newType *string, newSize *int64, iops *int64, throughput *int64) error
|
||||
DisconnectFromProvider() error
|
||||
DescribeVolumes(providerVolumesID []string) ([]VolumeProperties, error)
|
||||
TagVolumes(providerVolumesID []string, tags map[string]string) error
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user