From 33fbd01372604a68ff9dbce6020c6982a71bb76d Mon Sep 17 00:00:00 2001 From: rrackow Date: Mon, 21 Sep 2026 20:46:44 +0200 Subject: [PATCH 1/2] feat(providers): add STACKIT provider support --- docs/supported-platforms.md | 2 + internal/providers/stackit/stackit.go | 53 +++++++ internal/providers/stackit/stackit_test.go | 152 +++++++++++++++++++++ internal/register/providers.go | 1 + 4 files changed, 208 insertions(+) create mode 100644 internal/providers/stackit/stackit.go create mode 100644 internal/providers/stackit/stackit_test.go diff --git a/docs/supported-platforms.md b/docs/supported-platforms.md index 2744fff7d6..e266450702 100644 --- a/docs/supported-platforms.md +++ b/docs/supported-platforms.md @@ -31,6 +31,7 @@ Ignition is currently supported for the following platforms: * [IBM Power Systems Virtual Server] (`powervs`) - Ignition will read its configuration from the instance userdata. Cloud SSH keys are handled separately. * [QEMU] (`qemu`) - Ignition will read its configuration from the 'opt/com.coreos/config' key on the QEMU Firmware Configuration Device (available in QEMU 2.4.0 and higher). * [Scaleway] (`scaleway`) - Ignition will read its configuration from the instance userdata. Cloud SSH keys are handled separately. +* [STACKIT] (`stackit`) - Ignition will read its configuration from the instance userdata via the OpenStack-compatible metadata service at `http://169.254.169.254/openstack/latest/user_data`. Cloud SSH keys are handled separately. * [UpCloud] (`upcloud`) - Ignition will read its configuration from the instance userdata fetched from the metadata service (which is NOT enabled by default, make sure you enable it if you use custom images). Cloud SSH keys are handled separately. * [VirtualBox] (`virtualbox`) - Use the VirtualBox guest property `/Ignition/Config` to provide the config to the virtual machine. * [VMware] (`vmware`) - Use the VMware Guestinfo variables `ignition.config.data` and `ignition.config.data.encoding` to provide the config and its encoding to the virtual machine. Valid encodings are "", "base64", and "gzip+base64". Guestinfo variables can be provided directly or via an OVF environment, with priority given to variables specified directly. @@ -64,6 +65,7 @@ For most cloud providers, cloud SSH keys and custom network configuration are ha [IBM Power Systems Virtual Server]: https://www.ibm.com/products/power-virtual-server [QEMU]: https://www.qemu.org/ [Scaleway]: https://www.scaleway.com +[STACKIT]: https://www.stackit.cloud/ [UpCloud]: https://www.upcloud.com [VirtualBox]: https://www.virtualbox.org/ [VMware]: https://www.vmware.com/ diff --git a/internal/providers/stackit/stackit.go b/internal/providers/stackit/stackit.go new file mode 100644 index 0000000000..a2fceaa9e6 --- /dev/null +++ b/internal/providers/stackit/stackit.go @@ -0,0 +1,53 @@ +// Copyright 2026 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. + +// The STACKIT provider fetches a remote configuration from the +// OpenStack-compatible user-data metadata service. + +package stackit + +import ( + "net/url" + + "github.com/coreos/ignition/v2/config/v3_7_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" +) + +var ( + userdataURL = url.URL{ + Scheme: "http", + Host: "169.254.169.254", + Path: "openstack/latest/user_data", + } +) + +func init() { + platform.Register(platform.Provider{ + Name: "stackit", + Fetch: fetchConfig, + }) +} + +func fetchConfig(f *resource.Fetcher) (types.Config, report.Report, error) { + data, err := f.FetchToBuffer(userdataURL, resource.FetchOptions{}) + if err != nil && err != resource.ErrNotFound { + return types.Config{}, report.Report{}, err + } + + return util.ParseConfig(f.Logger, data) +} diff --git a/internal/providers/stackit/stackit_test.go b/internal/providers/stackit/stackit_test.go new file mode 100644 index 0000000000..4635c5983d --- /dev/null +++ b/internal/providers/stackit/stackit_test.go @@ -0,0 +1,152 @@ +// Copyright 2026 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 stackit + +import ( + "net/http" + "net/http/httptest" + "net/url" + "sync/atomic" + "testing" + + "github.com/coreos/ignition/v2/config/shared/errors" + "github.com/coreos/ignition/v2/config/v3_7_experimental/types" + "github.com/coreos/ignition/v2/internal/log" + "github.com/coreos/ignition/v2/internal/resource" + + "github.com/stretchr/testify/assert" +) + +func TestFetchConfig(t *testing.T) { + const validConfig = `{"ignition":{"version":"3.6.0"},"systemd":{"units":[{"name":"example.service","mask":true}]}}` + tests := []struct { + name string + status int + body string + retry bool + offline bool + wantErr error + wantFatal bool + wantRequests int32 + }{ + { + name: "valid config", + status: http.StatusOK, + body: validConfig, + wantRequests: 1, + }, + { + name: "missing userdata", + status: http.StatusNotFound, + body: "Not Found", + wantErr: errors.ErrEmpty, + wantRequests: 1, + }, + { + name: "empty userdata", + status: http.StatusOK, + wantErr: errors.ErrEmpty, + wantRequests: 1, + }, + { + name: "no content", + status: http.StatusNoContent, + wantErr: errors.ErrEmpty, + wantRequests: 1, + }, + { + name: "invalid config", + status: http.StatusOK, + body: `{"ignition":{"version":"3.6.0"},"storage":{"files":[{"path":"relative"}]}}`, + wantErr: errors.ErrInvalid, + wantFatal: true, + wantRequests: 1, + }, + { + name: "forbidden userdata", + status: http.StatusForbidden, + body: validConfig, + wantErr: resource.ErrFailed, + wantRequests: 1, + }, + { + name: "retry unavailable metadata service", + status: http.StatusOK, + body: validConfig, + retry: true, + wantRequests: 2, + }, + { + name: "offline", + status: http.StatusOK, + body: validConfig, + offline: true, + wantErr: resource.ErrNeedNet, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + var requests atomic.Int32 + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + attempt := requests.Add(1) + assert.Equal(t, http.MethodGet, r.Method) + assert.Equal(t, "/openstack/latest/user_data", r.URL.Path) + if test.retry && attempt == 1 { + w.WriteHeader(http.StatusServiceUnavailable) + return + } + w.WriteHeader(test.status) + _, _ = w.Write([]byte(test.body)) + })) + t.Cleanup(server.Close) + + serverURL, err := url.Parse(server.URL) + if err != nil { + t.Fatal(err) + } + originalURL := userdataURL + t.Cleanup(func() { userdataURL = originalURL }) + userdataURL.Host = serverURL.Host + + logger := log.New(true) + fetcher := resource.Fetcher{Logger: &logger, Offline: test.offline} + timeout := 5 + if err := fetcher.UpdateHttpTimeoutsAndCAs(types.Timeouts{HTTPTotal: &timeout}, nil, types.Proxy{}); err != nil { + t.Fatal(err) + } + + cfg, rpt, err := fetchConfig(&fetcher) + // The engine compares these errors directly, so preserve their identity. + if err != test.wantErr { + t.Fatalf("expected error %v, got %v", test.wantErr, err) + } + assert.Equal(t, test.wantFatal, rpt.IsFatal(), "unexpected report: %s", rpt.String()) + assert.Equal(t, test.wantRequests, requests.Load()) + if test.wantErr != nil { + assert.Equal(t, types.Config{}, cfg) + return + } + + assert.Equal(t, types.MaxVersion.String(), cfg.Ignition.Version) + if !assert.Len(t, cfg.Systemd.Units, 1) { + return + } + assert.Equal(t, "example.service", cfg.Systemd.Units[0].Name) + masked := true + assert.Equal(t, &masked, cfg.Systemd.Units[0].Mask) + }) + } +} diff --git a/internal/register/providers.go b/internal/register/providers.go index 7cde7f741c..ade48aacc4 100644 --- a/internal/register/providers.go +++ b/internal/register/providers.go @@ -40,6 +40,7 @@ import ( _ "github.com/coreos/ignition/v2/internal/providers/proxmoxve" _ "github.com/coreos/ignition/v2/internal/providers/qemu" _ "github.com/coreos/ignition/v2/internal/providers/scaleway" + _ "github.com/coreos/ignition/v2/internal/providers/stackit" _ "github.com/coreos/ignition/v2/internal/providers/upcloud" _ "github.com/coreos/ignition/v2/internal/providers/virtualbox" _ "github.com/coreos/ignition/v2/internal/providers/vmware" From da2a8fb95fc8f7a868b25391022cdb871c2cb749 Mon Sep 17 00:00:00 2001 From: Steven Presti Date: Tue, 3 Nov 2026 15:50:50 -0500 Subject: [PATCH 2/2] docs: add release note for STACKIT provider --- docs/release-notes.d/stackit-provider.feature.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 docs/release-notes.d/stackit-provider.feature.md diff --git a/docs/release-notes.d/stackit-provider.feature.md b/docs/release-notes.d/stackit-provider.feature.md new file mode 100644 index 0000000000..7c15e21228 --- /dev/null +++ b/docs/release-notes.d/stackit-provider.feature.md @@ -0,0 +1 @@ +Add support for the STACKIT platform.