Skip to content

Commit

Permalink
bottlerocket host update api integration
Browse files Browse the repository at this point in the history
Adds support for updating via the Bottlerocket Update API.
The new platform is used if the node is labeled
bottlerocket.aws/updater-interface-version=2.0.0
  • Loading branch information
etungsten committed Jul 10, 2020
1 parent 0527bd9 commit 4e7fd4f
Show file tree
Hide file tree
Showing 8 changed files with 485 additions and 23 deletions.
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ module github.com/bottlerocket-os/bottlerocket-update-operator
go 1.12

require (
github.com/coreos/go-systemd/v22 v22.0.0
github.com/godbus/dbus/v5 v5.0.3
github.com/Masterminds/semver v1.5.0
github.com/google/go-cmp v0.3.1 // indirect
github.com/googleapis/gnostic v0.3.1 // indirect
github.com/imdario/mergo v0.3.7 // indirect
github.com/karlseguin/ccache v2.0.3+incompatible
github.com/karlseguin/expect v1.0.1 // indirect
github.com/pkg/errors v0.8.1
github.com/sirupsen/logrus v1.4.2
github.com/stretchr/testify v1.3.0
github.com/wsxiaoys/terminal v0.0.0-20160513160801-0940f3fc43a0 // indirect
golang.org/x/crypto v0.0.0-20190829043050-9756ffdc2472 // indirect
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297 // indirect
Expand Down
6 changes: 2 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbt
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd/go.mod h1:64YHyfSL2R96J44Nlwm39UHepQbyR5q10x7iYa1ks2E=
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
Expand All @@ -25,8 +27,6 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd/v22 v22.0.0 h1:XJIw/+VlJ+87J+doOxznsAWIdmWuViOVhkQamW5YV28=
github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk=
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
Expand Down Expand Up @@ -58,8 +58,6 @@ github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nA
github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY=
github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I=
github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/godbus/dbus/v5 v5.0.3 h1:ZqHaoEF7TBzh4jzPmqVhE/5A1z9of6orkAe5uHoAeME=
github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d h1:3PaI8p3seN09VjbTYC/QWlUZdZ1qS1zGjy7LH2Wt07I=
github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
Expand Down
7 changes: 1 addition & 6 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (
"github.com/bottlerocket-os/bottlerocket-update-operator/pkg/controller"
"github.com/bottlerocket-os/bottlerocket-update-operator/pkg/k8sutil"
"github.com/bottlerocket-os/bottlerocket-update-operator/pkg/logging"
"github.com/bottlerocket-os/bottlerocket-update-operator/pkg/platform/updog"
"github.com/bottlerocket-os/bottlerocket-update-operator/pkg/sigcontext"
"github.com/pkg/errors"
"k8s.io/client-go/kubernetes"
Expand Down Expand Up @@ -89,11 +88,7 @@ func runController(ctx context.Context, kube kubernetes.Interface, nodeName stri

func runAgent(ctx context.Context, kube kubernetes.Interface, nodeName string) error {
log := logging.New("agent")
platform, err := updog.New()
if err != nil {
return errors.WithMessage(err, "could not setup platform for agent")
}
a, err := agent.New(log, kube, platform, nodeName)
a, err := agent.New(log, kube, nil, nodeName)
if err != nil {
return err
}
Expand Down
32 changes: 29 additions & 3 deletions pkg/agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package agent

import (
"context"
update_api "github.com/bottlerocket-os/bottlerocket-update-operator/pkg/platform/update-api"
"github.com/bottlerocket-os/bottlerocket-update-operator/pkg/platform/updog"
"os"
"time"

Expand Down Expand Up @@ -67,18 +69,42 @@ type proc interface {
KillProcess() error
}

func New(log logging.Logger, kube kubernetes.Interface, plat platform.Platform, nodeName string) (*Agent, error) {
func New(log logging.Logger, kube kubernetes.Interface, platform platform.Platform, nodeName string) (*Agent, error) {
if nodeName == "" {
return nil, errors.New("nodeName must be provided for Agent to manage")
}
var nodeclient corev1.NodeInterface
if kube != nil {
nodeclient = kube.CoreV1().Nodes()
// Determine which platform to use depending on the updater interface version
var node, err = nodeclient.Get(nodeName, v1meta.GetOptions{})
if err != nil {
return nil, errors.New("failed to retrieve node information")
}
// Get the updater interface version from the node label
var platformVersion = node.Labels[marker.UpdaterInterfaceVersionKey]
switch platformVersion {
case "2.0.0":
platform, err = update_api.New()
if err != nil {
return nil, errors.WithMessage(err, "could not setup Update API platform for agent")
}
// If the updater interface version is not specified, default back to using Updog as the platform
default:
log.Info("unknown platform version specified, defaulting to using updog")
fallthrough
case "1.0.0":
platform, err = updog.New()
if err != nil {
return nil, errors.WithMessage(err, "could not setup Updog platform for agent")
}
}
}

return &Agent{
log: log,
kube: kube,
platform: plat,
platform: platform,
poster: &k8sPoster{log, nodeclient},
proc: &osProc{},
nodeName: nodeName,
Expand Down Expand Up @@ -314,7 +340,7 @@ func (a *Agent) realize(in *intent.Intent) error {

case marker.NodeActionUnknown, marker.NodeActionStabilize:
log.Debug("sitrep")
_, err = a.platform.Status()
err = platform.Ping(a.platform)
if err != nil {
break
}
Expand Down
163 changes: 163 additions & 0 deletions pkg/platform/update-api/platform.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
package update_api

import (
"context"
"encoding/json"
"github.com/Masterminds/semver"
"github.com/bottlerocket-os/bottlerocket-update-operator/pkg/logging"
"github.com/bottlerocket-os/bottlerocket-update-operator/pkg/platform"
"github.com/pkg/errors"
"io/ioutil"
"net"
"net/http"
"time"
)

// Assert Update-API as a platform implementor.
var _ platform.Platform = (*Platform)(nil)

type Platform struct {
log logging.Logger
httpClient http.Client
}

func New() (*Platform, error) {
return &Platform{log: logging.New("platform"), httpClient: http.Client{
Transport: &http.Transport{
DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
return net.Dial("unix", bottlerocketApiSock)
},
},
// Set a 10 second timeout for all requests
Timeout: 10 * time.Second,
}}, nil
}

type statusResponse struct {
osVersion *semver.Version
}

func (sr *statusResponse) OK() bool {
// Bottlerocket OS version needs to be at least 0.4.1 to support the Update API
constraint, _ := semver.NewConstraint(">= 0.4.1")
return constraint.Check(sr.osVersion)
}

// Try to determine if the Bottlerocket version at least 0.4.1
func (p Platform) Status() (platform.Status, error) {
response, err := p.httpClient.Get("http://unix" + "/os")
if err != nil {
p.log.WithError(err).Error("error when trying to GET '/actions/refresh-updates'")
return nil, err
}
var osInfo interface{}
body, err := ioutil.ReadAll(response.Body)
if err != nil {
return nil, err
}
err = json.Unmarshal(body, osInfo)
if err != nil {
return nil, err
}
// Assert 'version_id' in the OS info json output as string
osVersion, err := semver.NewVersion(osInfo.(map[string]interface{})["version_id"].(string))
if err != nil {
return nil, errors.Wrap(err, "failed to parse 'version_id' field as semver")
}
return &statusResponse{osVersion: osVersion}, nil
}

type listAvailableResponse struct {
chosenUpdate *UpdateImage
}

func (lar *listAvailableResponse) Updates() []platform.Update {
if lar.chosenUpdate == nil {
return nil
}
updates := make([]platform.Update, 1)
updates[0] = lar.chosenUpdate
return updates
}

func (p Platform) ListAvailable() (platform.Available, error) {
p.log.Debug("fetching list of available updates")

// Refresh list of updates and check if there are any available
err := postAction(p.httpClient, "/actions/refresh-updates")
if err != nil {
return nil, err
}

updateStatus, err := getUpdateStatus(p.httpClient)
if updateStatus.MostRecentCommand.CmdType == refresh && updateStatus.MostRecentCommand.CmdStatus == Success {
// If there is no available update to update to
if updateStatus.ChosenUpdate == nil {
return &listAvailableResponse{chosenUpdate: nil}, err
}
return &listAvailableResponse{chosenUpdate: updateStatus.ChosenUpdate}, err
} else {
return &listAvailableResponse{chosenUpdate: nil}, errors.New("failed to refresh updates or update action performed out of band")
}
}

func (p Platform) Prepare(target platform.Update) error {
updateStatus, err := getUpdateStatus(p.httpClient)
if err != nil {
return err
}
if updateStatus.UpdateState != Available && updateStatus.UpdateState != Staged {
return errors.Errorf("unexpected update state: %s, expecting state to be 'Available' or 'Staged'. update action performed out of band?", updateStatus.UpdateState)
}

// Download the update and apply it to the inactive partition
err = postAction(p.httpClient, "/actions/prepare-update")
if err != nil {
return err
}

updateStatus, err = getUpdateStatus(p.httpClient)
if updateStatus.MostRecentCommand.CmdType != prepare || updateStatus.MostRecentCommand.CmdStatus != Success {
return errors.New("failed to prepare update or update action performed out of band")
}
return nil
}

func (p Platform) Update(target platform.Update) error {
updateStatus, err := getUpdateStatus(p.httpClient)
if err != nil {
return err
}
if updateStatus.UpdateState != Staged {
return errors.Errorf("unexpected update state: %s, expecting state to be 'Staged'. update action performed out of band?", updateStatus.UpdateState)
}

// Activate the prepared update
err = postAction(p.httpClient, "/actions/activate-update")
if err != nil {
return err
}

updateStatus, err = getUpdateStatus(p.httpClient)
if updateStatus.MostRecentCommand.CmdType != activate || updateStatus.MostRecentCommand.CmdStatus != Success {
return errors.New("failed to activate update or update action performed out of band")
}
return nil
}

func (p Platform) BootUpdate(target platform.Update, rebootNow bool) error {
updateStatus, err := getUpdateStatus(p.httpClient)
if err != nil {
return err
}
if updateStatus.UpdateState != Ready {
return errors.Errorf("unexpected update state: %s, expecting state to be 'Ready'. update action performed out of band?", updateStatus.UpdateState)
}

// Reboot the host into the activated update
err = postAction(p.httpClient, "/actions/reboot")
if err != nil {
return err
}
return nil
}
Loading

0 comments on commit 4e7fd4f

Please sign in to comment.