Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/release-notes.d/stackit-provider.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add support for the STACKIT platform.
2 changes: 2 additions & 0 deletions docs/supported-platforms.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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/
Expand Down
53 changes: 53 additions & 0 deletions internal/providers/stackit/stackit.go
Original file line number Diff line number Diff line change
@@ -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)
}
152 changes: 152 additions & 0 deletions internal/providers/stackit/stackit_test.go
Original file line number Diff line number Diff line change
@@ -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: "<html>Not Found</html>",
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)
})
}
}
1 change: 1 addition & 0 deletions internal/register/providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Loading