diff --git a/docs/supported-platforms.md b/docs/supported-platforms.md index f0318339ab..8dc0feb139 100644 --- a/docs/supported-platforms.md +++ b/docs/supported-platforms.md @@ -6,6 +6,7 @@ nav_order: 8 Ignition is currently only supported for the following platforms: +* [Akamai Connected Cloud] (`akamai`) - Ignition will read its configuration from the instance userdata. Cloud SSH keys and network configuration are handled separately. * [Alibaba Cloud] (`aliyun`) - Ignition will read its configuration from the instance userdata. Cloud SSH keys are handled separately. * [Apple Hypervisor] (`applehv`) - Ignition will read its configuration using an HTTP GET over a vsock connection with its host on port 1024. * [Amazon Web Services] (`aws`) - Ignition will read its configuration from the instance userdata. Cloud SSH keys are handled separately. @@ -36,6 +37,7 @@ Ignition is under active development, so this list may grow over time. For most cloud providers, cloud SSH keys and custom network configuration are handled by [Afterburn]. +[Akamai Connected Cloud]: https://www.linode.com [Alibaba Cloud]: https://www.alibabacloud.com/product/ecs [Apple Hypervisor]: https://developer.apple.com/documentation/hypervisor [Amazon Web Services]: https://aws.amazon.com/ec2/ diff --git a/internal/providers/akamai/akamai.go b/internal/providers/akamai/akamai.go new file mode 100644 index 0000000000..1bae313421 --- /dev/null +++ b/internal/providers/akamai/akamai.go @@ -0,0 +1,122 @@ +// Copyright 2024 CoreOS, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package akamai provides platform support for Akamai Connected Cloud +// (previously known as Linode). +package akamai + +import ( + "bytes" + "encoding/base64" + "errors" + "fmt" + "net/http" + "net/url" + + "github.com/coreos/ignition/v2/config/v3_5_experimental/types" + "github.com/coreos/ignition/v2/internal/platform" + "github.com/coreos/ignition/v2/internal/providers/util" + "github.com/coreos/ignition/v2/internal/resource" + + "github.com/coreos/vcontext/report" +) + +func init() { + platform.Register(platform.Provider{ + Name: "akamai", + Fetch: fetchConfig, + }) +} + +// HTTP headers. +const ( + // tokenTTLHeader is the name of the HTTP request header that must be + // set when making requests to [tokenURL] or [tokenURL6]. + tokenTTLHeader = "Metadata-Token-Expiry-Seconds" + + // tokenHeader is the name of the HTTP request header that callers must + // set when making requests to [userdataURL] or [userdataURL6]. + tokenHeader = "Metadata-Token" +) + +var ( + // IPv4 URLs. + tokenURL = url.URL{Scheme: "http", Host: "169.254.169.254", Path: "/v1/token"} + userdataURL = url.URL{Scheme: "http", Host: "169.254.169.254", Path: "/v1/user-data"} + + // IPv6 URLs (for reference). + // tokenURL6 = url.URL{Scheme: "http", Host: "[fd00:a9fe:a9fe::1]", Path: "/v1/token"} + // userdataURL6 = url.URL{Scheme: "http", Host: "[fd00:a9fe:a9fe::1]", Path: "/v1/user-data"} +) + +func fetchConfig(f *resource.Fetcher) (types.Config, report.Report, error) { + if f.Offline { + return types.Config{}, report.Report{}, resource.ErrNeedNet + } + + token, err := getToken(f) + if err != nil { + return types.Config{}, report.Report{}, fmt.Errorf("get token: %w", err) + } + + // NOTE: If we do not explicitly set the "Accept" header, it will be + // set by FetchToBuffer to a value that the Linode Metadata Service + // does not accept. + encoded, err := f.FetchToBuffer(userdataURL, resource.FetchOptions{ + Headers: http.Header{ + "Accept": []string{"text/plain"}, + tokenHeader: []string{string(token)}, + }, + }) + if err != nil { + return types.Config{}, report.Report{}, fmt.Errorf("fetch userdata: %w", err) + } + + // The Linode Metadata Service requires userdata to be base64-encoded + // when it is uploaded, so we will have to decode the response. + data := make([]byte, base64.StdEncoding.DecodedLen(len(encoded))) + if _, err := base64.StdEncoding.Decode(data, encoded); err != nil { + return types.Config{}, report.Report{}, fmt.Errorf("decode base64: %w", err) + } + + return util.ParseConfig(f.Logger, data) +} + +// getToken retrieves an authorization token to use for subsequent requests to +// Linode's Metadata Service. +// The returned token must be provided in the [tokenHeader] request header. +func getToken(f *resource.Fetcher) (token string, err error) { + // NOTE: This is using "text/plain" for content negotiation, just to + // skip the need to decode a JSON response. + // In the future, the accepted content type should probably become + // "application/vnd.coreos.ignition+json", but that will require + // support from Linode's Metadata Service API. + p, err := f.FetchToBuffer(tokenURL, resource.FetchOptions{ + HTTPVerb: http.MethodPut, + Headers: http.Header{ + "Accept": []string{"text/plain"}, + tokenTTLHeader: []string{"3600"}, + }, + }) + if err != nil { + return "", fmt.Errorf("fetch to buffer: %w", err) + } + + p = bytes.TrimSpace(p) + if len(p) == 0 { + return "", errors.New("receieved an empty token") + } + + return string(p), nil +} diff --git a/internal/register/providers.go b/internal/register/providers.go index e7a06b3827..5382e2f3a6 100644 --- a/internal/register/providers.go +++ b/internal/register/providers.go @@ -15,6 +15,7 @@ package register import ( + _ "github.com/coreos/ignition/v2/internal/providers/akamai" _ "github.com/coreos/ignition/v2/internal/providers/aliyun" _ "github.com/coreos/ignition/v2/internal/providers/applehv" _ "github.com/coreos/ignition/v2/internal/providers/aws"