resources: aws: ec2: Subscribe SNS endpoint to topic
This patch adds methods to subscribe and confirm the subscription to the sns topic.
This commit is contained in:
committed by
James Shubin
parent
966172eac6
commit
1162485c2c
@@ -20,8 +20,8 @@ package resources
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -53,6 +53,9 @@ const (
|
|||||||
SnsPrefix = Ec2Prefix + "sns-"
|
SnsPrefix = Ec2Prefix + "sns-"
|
||||||
// SnsTopicName is the name of the sns topic created by snsMakeTopic.
|
// SnsTopicName is the name of the sns topic created by snsMakeTopic.
|
||||||
SnsTopicName = SnsPrefix + "events"
|
SnsTopicName = SnsPrefix + "events"
|
||||||
|
// SnsSubscriptionProto is used to tell sns that the subscriber uses the http protocol.
|
||||||
|
// TODO: add https support
|
||||||
|
SnsSubscriptionProto = "http"
|
||||||
// SnsServerShutdownTimeout is the maximum number of seconds to wait for the http server to shutdown gracefully.
|
// SnsServerShutdownTimeout is the maximum number of seconds to wait for the http server to shutdown gracefully.
|
||||||
SnsServerShutdownTimeout = 30
|
SnsServerShutdownTimeout = 30
|
||||||
// waitTimeout is the duration in seconds of the timeout context in CheckApply.
|
// waitTimeout is the duration in seconds of the timeout context in CheckApply.
|
||||||
@@ -65,7 +68,7 @@ const (
|
|||||||
type awsEc2Event uint8
|
type awsEc2Event uint8
|
||||||
|
|
||||||
const (
|
const (
|
||||||
awsEc2EventServerReady awsEc2Event = iota
|
awsEc2EventWatchReady awsEc2Event = iota
|
||||||
awsEc2EventInstanceStopped
|
awsEc2EventInstanceStopped
|
||||||
awsEc2EventInstanceRunning
|
awsEc2EventInstanceRunning
|
||||||
awsEc2EventInstanceExists
|
awsEc2EventInstanceExists
|
||||||
@@ -103,6 +106,7 @@ type AwsEc2Res struct {
|
|||||||
Type string `yaml:"type"` // type of ec2 instance, eg: t2.micro
|
Type string `yaml:"type"` // type of ec2 instance, eg: t2.micro
|
||||||
ImageID string `yaml:"imageid"` // imageid must be available on the chosen region
|
ImageID string `yaml:"imageid"` // imageid must be available on the chosen region
|
||||||
|
|
||||||
|
WatchEndpoint string `yaml:"watchendpoint"` // the public url of the sns endpoint, eg: http://server:12345/
|
||||||
WatchListenAddr string `yaml:"watchlistenaddr"` // the local address or port that the sns listens on, eg: 10.0.0.0:23456 or 23456
|
WatchListenAddr string `yaml:"watchlistenaddr"` // the local address or port that the sns listens on, eg: 10.0.0.0:23456 or 23456
|
||||||
// UserData is used to run bash and cloud-init commands on first launch.
|
// UserData is used to run bash and cloud-init commands on first launch.
|
||||||
// See http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/user-data.html
|
// See http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/user-data.html
|
||||||
@@ -127,6 +131,13 @@ type chanStruct struct {
|
|||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// postData is the format of the messages received and decoded by snsPostHandler().
|
||||||
|
type postData struct {
|
||||||
|
Type string `json:"Type"`
|
||||||
|
Token string `json:"Token"`
|
||||||
|
Message string `json:"Message"`
|
||||||
|
}
|
||||||
|
|
||||||
// Default returns some sensible defaults for this resource.
|
// Default returns some sensible defaults for this resource.
|
||||||
func (obj *AwsEc2Res) Default() Res {
|
func (obj *AwsEc2Res) Default() Res {
|
||||||
return &AwsEc2Res{
|
return &AwsEc2Res{
|
||||||
@@ -185,6 +196,13 @@ func (obj *AwsEc2Res) Validate() error {
|
|||||||
return fmt.Errorf("imageid must be a valid ami available in the specified region")
|
return fmt.Errorf("imageid must be a valid ami available in the specified region")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if obj.WatchEndpoint == "" && obj.WatchListenAddr != "" {
|
||||||
|
return fmt.Errorf("you must set watchendpoint with watchlistenaddr to use http watch")
|
||||||
|
}
|
||||||
|
if obj.WatchEndpoint != "" && obj.WatchListenAddr == "" {
|
||||||
|
return fmt.Errorf("you must set watchendpoint with watchlistenaddr to use http watch")
|
||||||
|
}
|
||||||
|
|
||||||
return obj.BaseRes.Validate()
|
return obj.BaseRes.Validate()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -554,6 +572,16 @@ func (obj *AwsEc2Res) snsWatch() error {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
log.Printf("%s: Started SNS Endpoint", obj)
|
log.Printf("%s: Started SNS Endpoint", obj)
|
||||||
|
// Subscribing the endpoint to the topic needs to happen after starting
|
||||||
|
// the http server, so that the server can process the subscription
|
||||||
|
// confirmation. We won't drop incoming connections from aws by this
|
||||||
|
// point, because we've already opened the server listener. In the
|
||||||
|
// worst case scenario the incoming aws connections will be accepted
|
||||||
|
// but will block until our http server finishes getting ready in
|
||||||
|
// its goroutine.
|
||||||
|
if err := obj.snsSubscribe(obj.WatchEndpoint, obj.snsTopicArn); err != nil {
|
||||||
|
return errwrap.Wrapf(err, "error subscribing to sns topic")
|
||||||
|
}
|
||||||
// process events
|
// process events
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
@@ -568,6 +596,16 @@ func (obj *AwsEc2Res) snsWatch() error {
|
|||||||
if err := msg.err; err != nil {
|
if err := msg.err; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
// snsPostHandler sends the ready message after the
|
||||||
|
// subscription is confirmed. Once the subscription
|
||||||
|
// is confirmed, we are ready to receive events, so we
|
||||||
|
// can notify the engine that we're running.
|
||||||
|
if msg.event == awsEc2EventWatchReady {
|
||||||
|
if err := obj.Running(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
log.Printf("%s: State: %v", obj, msg.event)
|
log.Printf("%s: State: %v", obj, msg.event)
|
||||||
obj.StateOK(false)
|
obj.StateOK(false)
|
||||||
send = true
|
send = true
|
||||||
@@ -781,6 +819,9 @@ func (obj *AwsEc2Res) Compare(r Res) bool {
|
|||||||
if obj.UserData != res.UserData {
|
if obj.UserData != res.UserData {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if obj.WatchEndpoint != res.WatchEndpoint {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if obj.WatchListenAddr != res.WatchListenAddr {
|
if obj.WatchListenAddr != res.WatchListenAddr {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -831,9 +872,42 @@ func (obj *AwsEc2Res) snsPostHandler(w http.ResponseWriter, req *http.Request) {
|
|||||||
http.Error(w, "Invalid request method", http.StatusMethodNotAllowed)
|
http.Error(w, "Invalid request method", http.StatusMethodNotAllowed)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
post, _ := ioutil.ReadAll(req.Body)
|
// decode json
|
||||||
if obj.debug {
|
decoder := json.NewDecoder(req.Body)
|
||||||
log.Printf("%s: Post: %s", obj, string(post))
|
var post postData
|
||||||
|
if err := decoder.Decode(&post); err != nil {
|
||||||
|
http.Error(w, "Bad request", http.StatusBadRequest)
|
||||||
|
select {
|
||||||
|
case obj.awsChan <- &chanStruct{
|
||||||
|
err: errwrap.Wrapf(err, "error decoding incoming POST, check struct formatting"),
|
||||||
|
}:
|
||||||
|
case <-obj.closeChan:
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if post.Type == "SubscriptionConfirmation" {
|
||||||
|
if err := obj.snsConfirmSubscription(obj.snsTopicArn, post.Token); err != nil {
|
||||||
|
select {
|
||||||
|
case obj.awsChan <- &chanStruct{
|
||||||
|
err: errwrap.Wrapf(err, "error confirming subscription"),
|
||||||
|
}:
|
||||||
|
case <-obj.closeChan:
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Now that the subscription is confirmed, we can tell the
|
||||||
|
// engine we're running. If there is a delay between making the
|
||||||
|
// request and the subscription actually being confirmed,
|
||||||
|
// amazon will retry sending any new messages every 20 seconds
|
||||||
|
// for one minute. So, we won't miss any events. See the
|
||||||
|
// following for more details:
|
||||||
|
// http://docs.aws.amazon.com/sns/latest/dg/SendMessageToHttp.html#SendMessageToHttp.retry
|
||||||
|
select {
|
||||||
|
case obj.awsChan <- &chanStruct{
|
||||||
|
event: awsEc2EventWatchReady,
|
||||||
|
}:
|
||||||
|
case <-obj.closeChan:
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -867,13 +941,46 @@ func (obj *AwsEc2Res) snsDeleteTopic(topicArn string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// snsSubscribe subscribes the endpoint to the sns topic.
|
||||||
|
// Returning SubscriptionArn here is useless as it is still pending confirmation.
|
||||||
|
func (obj *AwsEc2Res) snsSubscribe(endpoint string, topicArn string) error {
|
||||||
|
// subscribe to the topic
|
||||||
|
subInput := &sns.SubscribeInput{
|
||||||
|
Endpoint: aws.String(endpoint),
|
||||||
|
Protocol: aws.String(SnsSubscriptionProto),
|
||||||
|
TopicArn: aws.String(topicArn),
|
||||||
|
}
|
||||||
|
_, err := obj.snsClient.Subscribe(subInput)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Printf("%s: Created Subscription", obj)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// snsConfirmSubscription confirms the sns subscription.
|
||||||
|
// Returning SubscriptionArn here is useless as it is still pending confirmation.
|
||||||
|
func (obj *AwsEc2Res) snsConfirmSubscription(topicArn string, token string) error {
|
||||||
|
// confirm the subscription
|
||||||
|
csInput := &sns.ConfirmSubscriptionInput{
|
||||||
|
Token: aws.String(token),
|
||||||
|
TopicArn: aws.String(topicArn),
|
||||||
|
}
|
||||||
|
_, err := obj.snsClient.ConfirmSubscription(csInput)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Printf("%s: Subscription Confirmed", obj)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Close cleans up when we're done. This is needed to delete some of the AWS
|
// Close cleans up when we're done. This is needed to delete some of the AWS
|
||||||
// objects created for the SNS endpoint.
|
// objects created for the SNS endpoint.
|
||||||
func (obj *AwsEc2Res) Close() error {
|
func (obj *AwsEc2Res) Close() error {
|
||||||
var errList error
|
var errList error
|
||||||
// clean up sns objects created by Init/snsWatch
|
// clean up sns objects created by Init/snsWatch
|
||||||
if obj.snsClient != nil {
|
if obj.snsClient != nil {
|
||||||
// delete the topic
|
// delete the topic and associated subscriptions
|
||||||
if err := obj.snsDeleteTopic(obj.snsTopicArn); err != nil {
|
if err := obj.snsDeleteTopic(obj.snsTopicArn); err != nil {
|
||||||
errList = multierr.Append(errList, err)
|
errList = multierr.Append(errList, err)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user