Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat.] Add AS Lifecycle Hook #732

Merged
merged 11 commits into from
Oct 7, 2024
43 changes: 41 additions & 2 deletions acceptance/openstack/autoscaling/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,31 @@ import (
"testing"

golangsdk "github.com/opentelekomcloud/gophertelekomcloud"
"github.com/opentelekomcloud/gophertelekomcloud/acceptance/clients"
"github.com/opentelekomcloud/gophertelekomcloud/acceptance/openstack"
"github.com/opentelekomcloud/gophertelekomcloud/acceptance/tools"
"github.com/opentelekomcloud/gophertelekomcloud/openstack/autoscaling/v1/configurations"
"github.com/opentelekomcloud/gophertelekomcloud/openstack/autoscaling/v1/groups"
"github.com/opentelekomcloud/gophertelekomcloud/openstack/common/pointerto"
"github.com/opentelekomcloud/gophertelekomcloud/openstack/smn/v2/topics"
th "github.com/opentelekomcloud/gophertelekomcloud/testhelper"
)

func CreateAutoScalingGroup(t *testing.T, client *golangsdk.ServiceClient, networkID, vpcID, asName string) string {
defaultSGID := openstack.DefaultSecurityGroup(t)

asCreateName := tools.RandomString("as-create-", 3)
keyPairName := clients.EnvOS.GetEnv("KEYPAIR_NAME")
imageID := clients.EnvOS.GetEnv("IMAGE_ID")
if keyPairName == "" || imageID == "" {
t.Skip("OS_KEYPAIR_NAME or OS_IMAGE_ID env vars is missing but AS Configuration test requires")
}

configID := CreateASConfig(t, client, asCreateName, imageID, keyPairName)

createOpts := groups.CreateOpts{
Name: asName,
Name: asName,
ConfigurationID: configID,
Networks: []groups.ID{
{
ID: networkID,
Expand All @@ -41,12 +54,17 @@ func CreateAutoScalingGroup(t *testing.T, client *golangsdk.ServiceClient, netwo
}

func DeleteAutoScalingGroup(t *testing.T, client *golangsdk.ServiceClient, groupID string) {
group, err := groups.Get(client, groupID)
th.AssertNoErr(t, err)
configID := group.ConfigurationID
t.Logf("Attempting to delete AutoScaling Group")
err := groups.Delete(client, groups.DeleteOpts{
err = groups.Delete(client, groups.DeleteOpts{
ScalingGroupId: groupID,
})
th.AssertNoErr(t, err)
t.Logf("Deleted AutoScaling Group: %s", groupID)

DeleteASConfig(t, client, configID)
}

func CreateASConfig(t *testing.T, client *golangsdk.ServiceClient, asCreateName string, imageID string, keyPairName string) string {
Expand Down Expand Up @@ -87,3 +105,24 @@ func DeleteASConfig(t *testing.T, client *golangsdk.ServiceClient, configID stri
th.AssertNoErr(t, err)
t.Logf("Deleted AutoScaling Configuration: %s", configID)
}

func GetNotificationTopicURN(topicName string) (string, error) {
client, _ := clients.NewSmnV2Client()

opts := topics.CreateOps{
Name: topicName,
}
topic, err := topics.Create(client, opts).Extract()

return topic.TopicUrn, err
}

func DeleteTopic(t *testing.T, topicURN string) {
client, _ := clients.NewSmnV2Client()
t.Logf("Attempting to Delete Topic: %s", topicURN)
err := topics.Delete(client, topicURN).ExtractErr()
if err != nil {
t.Logf("Error while deleting the topic: %s", topicURN)
}
t.Logf("Deleted Topic: %s", topicURN)
}
75 changes: 75 additions & 0 deletions acceptance/openstack/autoscaling/v1/lifecycle_hooks_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package v1

import (
"testing"

"github.com/opentelekomcloud/gophertelekomcloud/acceptance/clients"
"github.com/opentelekomcloud/gophertelekomcloud/acceptance/openstack/autoscaling"
"github.com/opentelekomcloud/gophertelekomcloud/acceptance/tools"
lifecyclehooks "github.com/opentelekomcloud/gophertelekomcloud/openstack/autoscaling/v1/lifecycle_hooks"
th "github.com/opentelekomcloud/gophertelekomcloud/testhelper"
)

func TestLifecycleHooksLifecycle(t *testing.T) {

networkID := clients.EnvOS.GetEnv("NETWORK_ID")
muneeb-jan marked this conversation as resolved.
Show resolved Hide resolved
vpcID := clients.EnvOS.GetEnv("VPC_ID")
if networkID == "" || vpcID == "" {
t.Skip("OS_NETWORK_ID or OS_VPC_ID env vars are missing but are required for AS Lifecycle Hooks test")
}

client, err := clients.NewAutoscalingV1Client()
th.AssertNoErr(t, err)

groupID := autoscaling.CreateAutoScalingGroup(t, client, networkID, vpcID, tools.RandomString("as-group-create-", 3))

topicName := tools.RandomString("as-lifecycle-hooks-topic-", 3)
t.Logf("Attempting to create Topic: %s", topicName)
topicURN, err := autoscaling.GetNotificationTopicURN(topicName)
if err != nil {
t.Logf("Error while creating the notification topic: %s", topicName)
}
lifecycleHookName := tools.RandomString("as-lifecycle-hook-create-", 3)
createOpts := lifecyclehooks.CreateOpts{
LifecycleHookName: lifecycleHookName,
LifecycleHookType: "INSTANCE_LAUNCHING",
NotificationTopicUrn: topicURN,
}

t.Logf("Attempting to create Lifecycle Hook")
lifecycleHook, err := lifecyclehooks.Create(client, createOpts, groupID)
th.AssertNoErr(t, err)
t.Logf("Created Lifecycle Hook: %s", lifecycleHook.LifecycleHookName)

requestedLifecycleHook, err := lifecyclehooks.Get(client, groupID, lifecycleHookName)
th.AssertNoErr(t, err)
th.AssertDeepEquals(t, requestedLifecycleHook, lifecycleHook)

updateOpts := lifecyclehooks.UpdateOpts{
LifecycleHookType: "INSTANCE_TERMINATING",
DefaultTimeout: 4800,
}
t.Logf("Attempting to update Lifecycle Hook")
lifecycleHook, err = lifecyclehooks.Update(client, updateOpts, groupID, lifecycleHookName)
th.AssertEquals(t, updateOpts.DefaultTimeout, lifecycleHook.DefaultTimeout)
th.AssertEquals(t, updateOpts.LifecycleHookType, lifecycleHook.LifecycleHookType)
th.AssertNoErr(t, err)
t.Logf("Updated Lifecycle Hook: %s", lifecycleHookName)

t.Logf("Listing all Lifecycle Hooks")
lifecycleHooks, err := lifecyclehooks.List(client, groupID)
th.AssertNoErr(t, err)
for _, lifecycleHook := range lifecycleHooks {
tools.PrintResource(t, lifecycleHook)
}

t.Cleanup(func() {
t.Logf("Attempting to delete Lifecycle Hook")
err = lifecyclehooks.Delete(client, groupID, lifecycleHookName)
th.AssertNoErr(t, err)
t.Logf("Deleted Lifecycle Hook: %s", lifecycleHookName)

autoscaling.DeleteTopic(t, topicURN)
autoscaling.DeleteAutoScalingGroup(t, client, groupID)
})
}
21 changes: 21 additions & 0 deletions openstack/autoscaling/v1/lifecycle_hooks/Common.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package lifecyclehooks

// CreateLifecycleHookResponse represents the response parameter struct for lifecycle hook.
type LifecycleHook struct {
// Specifies the lifecycle hook name.
LifecycleHookName string `json:"lifecycle_hook_name"`
// Specifies the lifecycle hook type. Values: INSTANCE_TERMINATING, INSTANCE_LAUNCHING
LifecycleHookType string `json:"lifecycle_hook_type"`
// Specifies the default lifecycle hook callback operation. Values: ABANDON, CONTINUE
DefaultResult string `json:"default_result"`
// Specifies the lifecycle hook timeout duration in seconds.
DefaultTimeout int `json:"default_timeout"`
// Specifies a unique topic in SMN for notification.
NotificationTopicUrn string `json:"notification_topic_urn"`
// Specifies the topic name in SMN.
NotificationTopicName string `json:"notification_topic_name"`
// Specifies the notification message.
NotificationMetadata string `json:"notification_metadata"`
// Specifies the UTC-compliant time when the lifecycle hook is created.
CreateTime string `json:"create_time"`
}
59 changes: 59 additions & 0 deletions openstack/autoscaling/v1/lifecycle_hooks/Create.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package lifecyclehooks

import (
golangsdk "github.com/opentelekomcloud/gophertelekomcloud"
"github.com/opentelekomcloud/gophertelekomcloud/internal/build"
"github.com/opentelekomcloud/gophertelekomcloud/internal/extract"
)

// CreateOpts represents the request parameter struct for lifecycle hook.
type CreateOpts struct {
// Specifies the lifecycle hook name. The name contains only letters, digits, underscores (_), and hyphens (-), and cannot exceed 32 characters.
LifecycleHookName string `json:"lifecycle_hook_name" required:"true"`
// Specifies the lifecycle hook type. Options:
// - INSTANCE_TERMINATING: The hook suspends the instance when it is terminated.
// - INSTANCE_LAUNCHING: The hook suspends the instance when it is started.
LifecycleHookType string `json:"lifecycle_hook_type" required:"true"`
// Specifies the default lifecycle hook callback operation. This operation is performed when the timeout duration expires.
// Options: ABANDON(Default), CONTINUE
// ABANDON:
// If an instance is starting, ABANDON indicates that your customized operations failed, and the instance will be terminated.
// In such a case, the scaling action fails, and you must create a new instance.
// If an instance is stopping, ABANDON allows instance termination BUT stops other lifecycle hooks.
// CONTINUE:
// If an instance is starting, CONTINUE indicates that your customized operations are successful and the instance can be used.
// If an instance is stopping, CONTINUE allows instance termination AND the completion of other lifecycle hooks.
DefaultResult string `json:"default_result,omitempty"`
// Specifies the lifecycle hook timeout duration, which ranges from 60 to 86400 seconds. The default value is 3600.
DefaultTimeout int `json:"default_timeout,omitempty"`
anton-sidelnikov marked this conversation as resolved.
Show resolved Hide resolved
// Specifies a unique topic in SMN. This parameter specifies a notification object for a lifecycle hook.
// When an instance is suspended by the lifecycle hook, the SMN service sends a notification to the object.
// This notification contains the basic instance information, your customized notification content, and the token for controlling lifecycle operations.
NotificationTopicUrn string `json:"notification_topic_urn" required:"true"`
// Specifies a customized notification, which contains no more than 256 characters. The message cannot contain the following characters: <>&'(){}.
// After a notification object is configured, the SMN service sends your customized notification to the object.
NotificationMetadata string `json:"notification_metadata,omitempty"`
}

// This function is used to create a lifecycle hook for an AS group. Up to five lifecycle hooks can be created for one AS group.
// After the creation, when the AS group is resized, the lifecycle hook suspends the involved instance and puts it to a Wait (Adding to AS group) or Wait (Removing from AS group) status.
// This status is retained until the timeout duration ends or you manually perform a callback.
// During the instance waiting duration, you can perform customized operations.
// For example, you can install or configure software on a newly started instance, or download the log file from the instance before the instance terminates.
func Create(client *golangsdk.ServiceClient, opts CreateOpts, asGroupId string) (*LifecycleHook, error) {
b, err := build.RequestBody(opts, "")
if err != nil {
return nil, err
}
// POST /autoscaling-api/v1/{project_id}/scaling_lifecycle_hook/{scaling_group_id}
raw, err := client.Post(client.ServiceURL("scaling_lifecycle_hook", asGroupId), b, nil, &golangsdk.RequestOpts{
OkCodes: []int{200},
})
if err != nil {
return nil, err
}

var res LifecycleHook
err = extract.Into(raw.Body, &res)
return &res, err
}
18 changes: 18 additions & 0 deletions openstack/autoscaling/v1/lifecycle_hooks/Delete.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package lifecyclehooks

import (
golangsdk "github.com/opentelekomcloud/gophertelekomcloud"
)

// This function is used to delete a specified lifecycle hook.
// When a scaling action is being performed in an AS group, the lifecycle hooks of the AS group cannot be deleted.
func Delete(client *golangsdk.ServiceClient, asGroupId string, lifecycleHookName string) error {
// DELETE /autoscaling-api/v1/{project_id}/scaling_lifecycle_hook/{scaling_group_id}/{lifecycle_hook_name}
_, err := client.Delete(client.ServiceURL("scaling_lifecycle_hook", asGroupId, lifecycleHookName), &golangsdk.RequestOpts{
OkCodes: []int{204},
})
if err != nil {
return err
}
return nil
}
21 changes: 21 additions & 0 deletions openstack/autoscaling/v1/lifecycle_hooks/Get.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package lifecyclehooks

import (
golangsdk "github.com/opentelekomcloud/gophertelekomcloud"
"github.com/opentelekomcloud/gophertelekomcloud/internal/extract"
)

// This function is used to query details about a specified lifecycle hook by AS group ID and lifecycle hook name.
func Get(client *golangsdk.ServiceClient, asGroupId string, lifecycleHookName string) (*LifecycleHook, error) {
// GET /autoscaling-api/v1/{project_id}/scaling_lifecycle_hook/{scaling_group_id}/{lifecycle_hook_name}
raw, err := client.Get(client.ServiceURL("scaling_lifecycle_hook", asGroupId, lifecycleHookName), nil, &golangsdk.RequestOpts{
OkCodes: []int{200},
})
if err != nil {
return nil, err
}

var res LifecycleHook
err = extract.Into(raw.Body, &res)
return &res, err
}
27 changes: 27 additions & 0 deletions openstack/autoscaling/v1/lifecycle_hooks/List.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package lifecyclehooks

import (
golangsdk "github.com/opentelekomcloud/gophertelekomcloud"
"github.com/opentelekomcloud/gophertelekomcloud/internal/extract"
)

// This function is used to query lifecycle hooks by AS group ID.
func List(client *golangsdk.ServiceClient, asGroupId string) ([]LifecycleHook, error) {
// GET https://{Endpoint}/autoscaling-api/v1/{project_id}/scaling_lifecycle_hook/{scaling_group_id}/list
raw, err := client.Get(client.ServiceURL("scaling_lifecycle_hook", asGroupId, "list"), nil, &golangsdk.RequestOpts{
OkCodes: []int{200},
})
if err != nil {
return nil, err
}

var res ListLifeCycleHooksResponse
err = extract.Into(raw.Body, &res)
return res.LifecycleHooks, err

}

type ListLifeCycleHooksResponse struct {
// Specifies lifecycle hooks
LifecycleHooks []LifecycleHook `json:"lifecycle_hooks"`
}
54 changes: 54 additions & 0 deletions openstack/autoscaling/v1/lifecycle_hooks/Update.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package lifecyclehooks

import (
golangsdk "github.com/opentelekomcloud/gophertelekomcloud"
"github.com/opentelekomcloud/gophertelekomcloud/internal/build"
"github.com/opentelekomcloud/gophertelekomcloud/internal/extract"
)

// UpdateOpts represents the request parameter struct for lifecycle hook.
type UpdateOpts struct {
// Specifies the lifecycle hook type. Options:
// - INSTANCE_TERMINATING: The hook suspends the instance when it is terminated.
// - INSTANCE_LAUNCHING: The hook suspends the instance when it is started.
LifecycleHookType string `json:"lifecycle_hook_type,omitempty"`
// Specifies the default lifecycle hook callback operation. This operation is performed when the timeout duration expires.
// Options: ABANDON(Default), CONTINUE
// ABANDON:
// If an instance is starting, ABANDON indicates that your customized operations failed, and the instance will be terminated.
// In such a case, the scaling action fails, and you must create a new instance.
// If an instance is stopping, ABANDON allows instance termination BUT stops other lifecycle hooks.
// CONTINUE:
// If an instance is starting, CONTINUE indicates that your customized operations are successful and the instance can be used.
// If an instance is stopping, CONTINUE allows instance termination AND the completion of other lifecycle hooks.
DefaultResult string `json:"default_result,omitempty"`
// Specifies the lifecycle hook timeout duration, which ranges from 60 to 86400 seconds. The default value is 3600.
DefaultTimeout int `json:"default_timeout,omitempty"`
anton-sidelnikov marked this conversation as resolved.
Show resolved Hide resolved
// Specifies a unique topic in SMN. This parameter specifies a notification object for a lifecycle hook.
// When an instance is suspended by the lifecycle hook, the SMN service sends a notification to the object.
// This notification contains the basic instance information, your customized notification content, and the token for controlling lifecycle operations.
NotificationTopicUrn string `json:"notification_topic_urn,omitempty"`
// Specifies a customized notification, which contains no more than 256 characters. The message cannot contain the following characters: <>&'(){}.
// After a notification object is configured, the SMN service sends your customized notification to the object.
NotificationMetadata string `json:"notification_metadata,omitempty"`
}

// This function is used to update the information about a specified lifecycle hook.
func Update(client *golangsdk.ServiceClient, opts UpdateOpts, asGroupId string, lifecycleHookName string) (*LifecycleHook, error) {
b, err := build.RequestBody(opts, "")
if err != nil {
return nil, err
}

// PUT /autoscaling-api/v1/{project_id}/scaling_lifecycle_hook/{scaling_group_id}/{lifecycle_hook_name}
raw, err := client.Put(client.ServiceURL("scaling_lifecycle_hook", asGroupId, lifecycleHookName), b, nil, &golangsdk.RequestOpts{
OkCodes: []int{200},
})
if err != nil {
return nil, err
}

var res LifecycleHook
err = extract.Into(raw.Body, &res)
return &res, err
}