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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,10 @@
- [v0.5.0](services/telemetryrouter/CHANGELOG.md#v050)
- **Improvement:** Add validation for `Description` field
- `v1api`: **Improvement:** Add validation for `Description` field
- `ufw`:
- [v1.0.0](services/ufw/CHANGELOG.md#v100)
- **New:** STACKIT UFW service

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- **New:** STACKIT UFW service
- **New:** STACKIT Unified Firewall (UFW) service

- **Feature:** Add waiter and example methods for the API
- `valkey`:
- [v0.2.0](services/valkey/CHANGELOG.md#v020)
- `v2api`:
Expand Down
15 changes: 15 additions & 0 deletions examples/ufw/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module github.com/stackitcloud/stackit-sdk-go/examples/ufw

go 1.25.9

replace github.com/stackitcloud/stackit-sdk-go/services/ufw => ../../services/ufw

require (
github.com/stackitcloud/stackit-sdk-go/core v0.26.0
github.com/stackitcloud/stackit-sdk-go/services/ufw v0.0.0-00010101000000-000000000000
)

require (
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
github.com/google/uuid v1.6.0 // indirect
)
8 changes: 8 additions & 0 deletions examples/ufw/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/stackitcloud/stackit-sdk-go/core v0.26.0 h1:jQEb9gkehfp6VCP6TcYk7BI10cz4l0KM2L6hqYBH2QA=
github.com/stackitcloud/stackit-sdk-go/core v0.26.0/go.mod h1:WU1hhxnjXw2EV7CYa1nlEvNpMiRY6CvmIOaHuL3pOaA=
240 changes: 240 additions & 0 deletions examples/ufw/ufw.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
package main

import (
"context"
"fmt"
"os"
"reflect"
"strings"

"github.com/stackitcloud/stackit-sdk-go/core/config"
ufw "github.com/stackitcloud/stackit-sdk-go/services/ufw/v1api"
"github.com/stackitcloud/stackit-sdk-go/services/ufw/v1api/wait"
)

func main() {
region := "eu01" // Region where the resources will be created
projectId := "PROJECT_ID" // UUID of your STACKIT project
instanceId := "INSTANCE_ID" // UUID of the instance to which the firewall rule will be attached
productType := "PRODUCT_TYPE" // Type of the instance to which the firewall rule will be attached (e.g. "redis", but you can get them from provider-options route)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the product type not limited to Edge Cloud for the beginning? If yes, I would prefer that edge is mentioned in the example instead of redis.

ufwRuleType := "ACL" // Type of the rule that you want to create (ACL, SecurityRule, PublicIp, but you can get them from provider-options route

ctx := context.Background()

token := ""
ufwClient, err := ufw.NewAPIClient(config.WithToken(token))
if err != nil {
fmt.Fprintf(os.Stderr, "[UFW] Creating API client: %v\n", err)
os.Exit(1)
}

// List all firewall rules
listUFWRules(ctx, ufwClient, projectId, region)

// Create a new firewall rule
description := "Created from SDK"
rulePayloadToCreate := ufw.CreateRulePayload{
InstanceId: instanceId,
Product: productType,
SourceIP: "11.11.11.11/32",
Type: ufwRuleType,
Description: &description,
}

createdRuleResponse, err := createFirewallRule(ctx, ufwClient, projectId, region, &rulePayloadToCreate)

if err != nil {
fmt.Fprintf(os.Stderr, "[UFW] Error when creating firewall rule: %v\n", err)
return
}

fmt.Printf("Created firewall rule response: %+v\n", createdRuleResponse)

// Get the firewall rule
testGetRule, err := getFirewallRule(ctx, ufwClient, projectId, region, *createdRuleResponse.RefId)
if err != nil {
fmt.Fprintf(os.Stderr, "[UFW] Error when getting firewall rule: %v\n", err)
return
}

fmt.Printf("Firewall rule details: %+v\n", testGetRule)

if err := verifyPayloadMatch(testGetRule, rulePayloadToCreate); err != nil {
fmt.Fprintf(os.Stderr, "[UFW] Verification failed after creation:\n%v\n", err)
return
}
fmt.Println("Created rule fields verified successfully.")

// Update the firewall rule
rulePayloadToUpdate := ufw.UpdateRulePayload{
SourceIP: "22.22.22.22/32",
}

updatedRuleResponse, err := updateFirewallRule(ctx, ufwClient, projectId, region, *createdRuleResponse.RefId, rulePayloadToUpdate)

if err != nil {
fmt.Fprintf(os.Stderr, "[UFW] Error when updating firewall rule: %v\n", err)
return
}

fmt.Printf("Updated firewall rule details: %+v\n", updatedRuleResponse)

testGetRule, err = getFirewallRule(ctx, ufwClient, projectId, region, *updatedRuleResponse.RefId)
if err != nil {
fmt.Fprintf(os.Stderr, "[UFW] Error when getting updated firewall rule: %v\n", err)
return
}

if err := verifyPayloadMatch(testGetRule, rulePayloadToUpdate); err != nil {
fmt.Fprintf(os.Stderr, "[UFW] Verification failed after update:\n%v\n", err)
return
}
fmt.Println("Updated rule fields verified successfully.")

// Delete the firewall rule
err = deleteFirewallRule(ctx, ufwClient, projectId, region, *updatedRuleResponse.RefId)
if err != nil {
fmt.Fprintf(os.Stderr, "[UFW] Error when deleting firewall rule: %v\n", err)
return
}

_, err = getFirewallRule(ctx, ufwClient, projectId, region, *updatedRuleResponse.RefId)
if !strings.Contains(err.Error(), "404") {
fmt.Fprintf(os.Stderr, "[UFW] Error while verifying deleted rule: %v\n", err)
return
}

fmt.Println("All firewall rules successfully tested and cleaned up.")
}

func listUFWRules(ctx context.Context, ufwClient *ufw.APIClient, projectId, region string) {
listRulesResponse, err := ufwClient.DefaultAPI.ListRules(ctx, projectId, region).Execute()
if err != nil {
fmt.Fprintf(os.Stderr, "[UFW] Error when listing firewall rules: %v\n", err)
return
}

fmt.Println("List of firewall rules:")
for i := range listRulesResponse.Rules {
fmt.Printf("%+v\n", listRulesResponse.Rules[i])
}
}

func getFirewallRule(ctx context.Context, ufwClient *ufw.APIClient, projectId, region, ruleId string) (*ufw.RuleResponse, error) {
return ufwClient.DefaultAPI.GetRule(ctx, projectId, region, ruleId).Execute()
}

func createFirewallRule(ctx context.Context, ufwClient *ufw.APIClient, projectId, region string, payload *ufw.CreateRulePayload) (*ufw.SecurityRuleSuccessfullyCreatedResponse, error) {
createdFirewallRule, err := ufwClient.DefaultAPI.CreateRule(ctx, projectId, region).CreateRulePayload(*payload).Execute()
if err != nil {
return nil, err
}

createdFirewallRuleId := createdFirewallRule.RefId
fmt.Printf("Created firewall rule with ID: %s\n", *createdFirewallRuleId)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This message is a bit misleading. Actually here the creation is triggered and waiter starts.
So we should write sth like [UFW] Triggered creation of firewall rule with ID.....


_, err = wait.CreateRuleWaitHandler(ctx, ufwClient.DefaultAPI, projectId, region, *createdFirewallRuleId).WaitWithContext(ctx)
if err != nil {
return nil, err
}

return createdFirewallRule, nil
}

func updateFirewallRule(ctx context.Context, ufwClient *ufw.APIClient, projectId, region, ruleId string, payload ufw.UpdateRulePayload) (*ufw.SecurityRuleSuccessfullyCreatedResponse, error) {
updatedFirewallRule, err := ufwClient.DefaultAPI.UpdateRule(ctx, projectId, region, ruleId).UpdateRulePayload(payload).Execute()
if err != nil {
return nil, err
}

fmt.Printf("Updated firewall rule with ID: %s\n", *updatedFirewallRule.RefId)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
fmt.Printf("Updated firewall rule with ID: %s\n", *updatedFirewallRule.RefId)
fmt.Printf("[UFW] Triggered update of firewall rule with ID: %s\n", *updatedFirewallRule.RefId)


_, err = wait.UpdateRuleWaitHandler(ctx, ufwClient.DefaultAPI, projectId, region, *updatedFirewallRule.RefId).WaitWithContext(ctx)
if err != nil {
return nil, err
}

return updatedFirewallRule, nil
}

func deleteFirewallRule(ctx context.Context, ufwClient *ufw.APIClient, projectId, region, ruleId string) error {
deleteRuleResponse, err := ufwClient.DefaultAPI.DeleteRule(ctx, projectId, region, ruleId).Execute()
if err != nil {
return err
}

fmt.Printf("Deleted firewall rule with ID: %s\n", ruleId)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
fmt.Printf("Deleted firewall rule with ID: %s\n", ruleId)
fmt.Printf("[UFW] Triggered deletion of firewall rule with ID: %s\n", ruleId)

fmt.Printf("Deleted firewall rule response: %+v\n", deleteRuleResponse)

_, err = wait.DeleteRuleWaitHandler(ctx, ufwClient.DefaultAPI, projectId, region, ruleId).WaitWithContext(ctx)
if err != nil {
return err
}

return nil
}

func verifyPayloadMatch(actual, expected any) error {
var mismatches []string

actualVal := reflect.ValueOf(actual)
if actualVal.Kind() == reflect.Pointer {
actualVal = actualVal.Elem()
}

expectedVal := reflect.ValueOf(expected)
if expectedVal.Kind() == reflect.Pointer {
expectedVal = expectedVal.Elem()
}

expectedType := expectedVal.Type()

for i := 0; i < expectedVal.NumField(); i++ {
fieldName := expectedType.Field(i).Name

if fieldName == "AdditionalProperties" {
continue // AdditionalProperties field is not part of the API response struct
}

if fieldName == "Description" {
continue // Description field is overridden by the API, will be fixed in the future
}

expectedField := expectedVal.Field(i)

// Look for a matching field in the API response struct
actualField := actualVal.FieldByName(fieldName)
if !actualField.IsValid() {
continue // Field exists in payload but not in response struct, safe to skip
}

// Dereference pointers cleanly to get string representations
expStr := formatReflectValue(expectedField)
actStr := formatReflectValue(actualField)

// Skip uninitialized/nil fields in the expected payload
// (e.g. fields omitted from an UpdateRulePayload)
if expStr == "" || expStr == "<nil>" {
continue
}

if expStr != actStr {
mismatches = append(mismatches, fmt.Sprintf("%s: expected %q, got %q", fieldName, expStr, actStr))
}
}

if len(mismatches) > 0 {
return fmt.Errorf("field mismatches found:\n- %s", strings.Join(mismatches, "\n- "))
}
return nil
}

func formatReflectValue(v reflect.Value) string {
if v.Kind() == reflect.Pointer {
if v.IsNil() {
return "<nil>"
}
v = v.Elem()
}
return fmt.Sprintf("%v", v.Interface())
}
1 change: 1 addition & 0 deletions go.work
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ use (
./examples/sqlserverflex
./examples/telemetrylink
./examples/telemetryrouter
./examples/ufw
./examples/valkey
./examples/vpn
./examples/waiter
Expand Down
3 changes: 3 additions & 0 deletions services/ufw/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## v1.0.0
- **New:** STACKIT UFW service

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- **New:** STACKIT UFW service
- **New:** STACKIT Unified Firewall (UFW) service

- **Feature:** Add waiter and example methods for the API
2 changes: 1 addition & 1 deletion services/ufw/LICENSE.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.

Copyright [yyyy] [name of copyright owner]
Copyright 2026 Schwarz IT KG

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down
2 changes: 2 additions & 0 deletions services/ufw/NOTICE.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
STACKIT UFW SDK for Go
Copyright 2026 Schwarz IT KG
1 change: 1 addition & 0 deletions services/ufw/VERSION
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
v1.0.0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really want to start with v1? Other services mostly start with v0.1.0

5 changes: 4 additions & 1 deletion services/ufw/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ module github.com/stackitcloud/stackit-sdk-go/services/ufw

go 1.25

require github.com/stackitcloud/stackit-sdk-go/core v0.26.0
require (
github.com/google/go-cmp v0.7.0
github.com/stackitcloud/stackit-sdk-go/core v0.26.0
)

require (
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
Expand Down
2 changes: 1 addition & 1 deletion services/ufw/oas_commit
Original file line number Diff line number Diff line change
@@ -1 +1 @@
127d9fd6bc674ed6ae757e63103645d95f7163a5
127d9fd6bc674ed6ae757e63103645d95f7163a5
42 changes: 42 additions & 0 deletions services/ufw/v1api/wait/wait.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package wait

import (
"context"
"errors"
"net/http"
"time"

"github.com/stackitcloud/stackit-sdk-go/core/wait"
ufw "github.com/stackitcloud/stackit-sdk-go/services/ufw/v1api"
)

func CreateRuleWaitHandler(ctx context.Context, a ufw.DefaultAPI, projectId, region, ruleId string) *wait.AsyncActionHandler[ufw.RuleResponse] {
return ruleWaitHandler(ctx, a, projectId, region, ruleId, []ufw.RuleResponseStatus{ufw.RULERESPONSESTATUS_ACTIVE}, nil)
}

func UpdateRuleWaitHandler(ctx context.Context, a ufw.DefaultAPI, projectId, region, ruleId string) *wait.AsyncActionHandler[ufw.RuleResponse] {
return ruleWaitHandler(ctx, a, projectId, region, ruleId, []ufw.RuleResponseStatus{ufw.RULERESPONSESTATUS_ACTIVE}, nil)
}

func DeleteRuleWaitHandler(ctx context.Context, a ufw.DefaultAPI, projectId, region, ruleId string) *wait.AsyncActionHandler[ufw.RuleResponse] {
return ruleWaitHandler(ctx, a, projectId, region, ruleId, nil, []int{http.StatusNotFound})
}

func ruleWaitHandler(ctx context.Context, a ufw.DefaultAPI, projectId, region, ruleId string, activeStates []ufw.RuleResponseStatus, deleteHttpErrorStatusCodes []int) *wait.AsyncActionHandler[ufw.RuleResponse] {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you rename this function to sth like createOrUpdateRuleWaitHandler to keep the standard implementation of SDK. We try to follow this naming convention since some time.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

func createOrUpdateInstanceWaitHandler(ctx context.Context, client logme.DefaultAPI, projectId, region, instanceId string) *wait.AsyncActionHandler[logme.Instance] {

for example

waitConfig := wait.WaiterHelper[ufw.RuleResponse, ufw.RuleResponseStatus]{
FetchInstance: a.GetRule(ctx, projectId, region, ruleId).Execute,
GetState: func(ruleResp *ufw.RuleResponse) (ufw.RuleResponseStatus, error) {
if ruleResp == nil {
return "", errors.New("empty response")
}
return ruleResp.Status, nil
},
ActiveState: activeStates,
ErrorState: []ufw.RuleResponseStatus{ufw.RULERESPONSESTATUS_ERROR},
DeleteHttpErrorStatusCodes: deleteHttpErrorStatusCodes,
}

handler := wait.New(waitConfig.Wait())
handler.SetTimeout(5 * time.Minute)
return handler
}
Loading