Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
8712b73
add continue attribute for observability service alert config
PatrickKoss Sep 15, 2025
7ef4213
adjust the acceptance test observability
PatrickKoss Sep 15, 2025
702122b
adjust docs
PatrickKoss Sep 15, 2025
56eb265
add continue in another case
PatrickKoss Sep 17, 2025
b405ce7
Merge branch 'main' into main
PatrickKoss Sep 18, 2025
188f0b7
Merge branch 'stackitcloud:main' into main
PatrickKoss Sep 19, 2025
13fdd53
remove continue attribute from root
PatrickKoss Sep 19, 2025
113bbb9
fix acc test
PatrickKoss Sep 19, 2025
e073ec2
Merge branch 'stackitcloud:main' into main
PatrickKoss Sep 19, 2025
8118f17
fix docs
PatrickKoss Sep 19, 2025
3e1a403
fix unit tests
PatrickKoss Sep 19, 2025
039719f
remove route types
PatrickKoss Sep 19, 2025
6ffe516
Merge branch 'main' into main
rubenhoenle Sep 19, 2025
e74f9f8
Merge branch 'stackitcloud:main' into main
PatrickKoss Oct 29, 2025
4e99f0d
Merge branch 'stackitcloud:main' into main
PatrickKoss Nov 3, 2025
55183c5
Merge branch 'stackitcloud:main' into main
PatrickKoss Nov 10, 2025
ee3a0c8
add skip wait and set partial model
PatrickKoss Nov 10, 2025
76fc503
fix linting errors
PatrickKoss Nov 10, 2025
de09817
revert formatting
PatrickKoss Nov 11, 2025
e7649c2
revert formatting
PatrickKoss Nov 11, 2025
037cece
import state
PatrickKoss Nov 11, 2025
265836f
downlint lint from releases + remove read id check
PatrickKoss Nov 12, 2025
ba8ecc8
Merge branch 'main' into feature/dns-skip-wait
PatrickKoss Nov 12, 2025
1196efb
fix pipeline linting
PatrickKoss Nov 12, 2025
50f1f37
adjust SetModelFieldsToNull to handle complex objects and lists
PatrickKoss Nov 13, 2025
873f875
fix linting
PatrickKoss Nov 13, 2025
6e89bf9
fix linting
PatrickKoss Nov 13, 2025
b769ba1
add dns wait warn log for tf idempotency
PatrickKoss Nov 14, 2025
f65f2ff
Merge branch 'stackitcloud:main' into feature/dns-skip-wait
PatrickKoss Nov 19, 2025
2f8850c
change construction of minimal state
PatrickKoss Nov 19, 2025
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
13 changes: 11 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
ROOT_DIR ?= $(shell git rev-parse --show-toplevel)
SCRIPTS_BASE ?= $(ROOT_DIR)/scripts
BIN_DIR ?= $(ROOT_DIR)/bin

# https://github.com/golangci/golangci-lint/releases
GOLANGCI_LINT_VERSION = 1.64.8
GOLANGCI_LINT = $(BIN_DIR)/golangci-lint

# SETUP AND TOOL INITIALIZATION TASKS
project-help:
Expand All @@ -8,10 +13,14 @@ project-help:
project-tools:
@$(SCRIPTS_BASE)/project.sh tools

# GOLANGCI-LINT INSTALLATION
$(GOLANGCI_LINT):
@GOLANGCI_LINT_VERSION=$(GOLANGCI_LINT_VERSION) $(SCRIPTS_BASE)/install-golangci-lint.sh

# LINT
lint-golangci-lint:
lint-golangci-lint: $(GOLANGCI_LINT)
@echo "Linting with golangci-lint"
@$(SCRIPTS_BASE)/lint-golangci-lint.sh
@$(SCRIPTS_BASE)/lint-golangci-lint.sh $(GOLANGCI_LINT)

lint-tf:
@echo "Linting terraform files"
Expand Down
42 changes: 42 additions & 0 deletions scripts/install-golangci-lint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#!/usr/bin/env bash
set -e
. $(dirname ${0})/utility.sh

BINARY_NAME=golangci-lint
INSTALL_TO=${BIN_DIR}/${BINARY_NAME}

install() {
echo " installing ${BINARY_NAME} ${GOLANGCI_LINT_VERSION}"

TYPE=windows
if [[ "${OSTYPE}" == linux* ]]; then
TYPE=linux
elif [[ "${OSTYPE}" == darwin* ]]; then
TYPE=darwin
fi

case $(uname -m) in
arm64|aarch64)
ARCH=arm64
;;
*)
ARCH=amd64
;;
esac

BASE_URL=https://github.com/golangci/golangci-lint/releases/download/v${GOLANGCI_LINT_VERSION}
URL=${BASE_URL}/golangci-lint-${GOLANGCI_LINT_VERSION}-${TYPE}-${ARCH}.tar.gz
echo " Downloading: ${URL}"
download ${URL} | tar --extract --gzip --strip-components 1 --preserve-permissions -C ${BIN_DIR} -f-

# Ensure the binary has the correct name
if [ -f "${BIN_DIR}/golangci-lint" ] && [ "${BIN_DIR}/golangci-lint" != "${INSTALL_TO}" ]; then
mv "${BIN_DIR}/golangci-lint" "${INSTALL_TO}"
fi
}

get_version() {
${INSTALL_TO} version 2>/dev/null | awk '{print $4}'
}

update_if_necessary ${GOLANGCI_LINT_VERSION}
13 changes: 7 additions & 6 deletions scripts/lint-golangci-lint.sh
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
#!/usr/bin/env bash
# This script lints the SDK modules and the internal examples
# Pre-requisites: golangci-lint
# Pre-requisites: golangci-lint (provided by Makefile or system)
set -eo pipefail

ROOT_DIR=$(git rev-parse --show-toplevel)
GOLANG_CI_YAML_PATH="${ROOT_DIR}/golang-ci.yaml"
GOLANG_CI_ARGS="--allow-parallel-runners --timeout=5m --config=${GOLANG_CI_YAML_PATH}"

if type -p golangci-lint >/dev/null; then
:
else
echo "golangci-lint not installed, unable to proceed."
# Use provided golangci-lint binary or fallback to system installation
GOLANGCI_LINT_BIN="${1:-golangci-lint}"

if [ ! -x "${GOLANGCI_LINT_BIN}" ] && ! type -p "${GOLANGCI_LINT_BIN}" >/dev/null; then
echo "golangci-lint not found at ${GOLANGCI_LINT_BIN} and not installed in PATH, unable to proceed."
exit 1
fi

cd ${ROOT_DIR}
golangci-lint run ${GOLANG_CI_ARGS}
${GOLANGCI_LINT_BIN} run ${GOLANG_CI_ARGS}
46 changes: 46 additions & 0 deletions scripts/utility.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#!/usr/bin/env bash
# Common utility functions for tool installation scripts

ROOT_DIR=$(git rev-parse --show-toplevel)
BIN_DIR="${ROOT_DIR}/bin"

# Ensure bin directory exists
mkdir -p "${BIN_DIR}"

# Download function using curl
download() {
local URL=$1
if command -v curl &> /dev/null; then
curl -sSfL "${URL}"
elif command -v wget &> /dev/null; then
wget -qO- "${URL}"
else
echo "Error: Neither curl nor wget found. Please install one of them."
exit 1
fi
}

# Update tool if necessary
update_if_necessary() {
local EXPECTED_VERSION=$1

if [ -x "${INSTALL_TO}" ]; then
CURRENT_VERSION=$(get_version 2>/dev/null || echo "")
if [ "${CURRENT_VERSION}" = "${EXPECTED_VERSION}" ]; then
echo " ${BINARY_NAME} ${EXPECTED_VERSION} already installed"
return 0
else
echo " ${BINARY_NAME} version mismatch (current: ${CURRENT_VERSION}, expected: ${EXPECTED_VERSION})"
echo " updating to ${EXPECTED_VERSION}..."
fi
fi

install

INSTALLED_VERSION=$(get_version 2>/dev/null || echo "unknown")
if [ "${INSTALLED_VERSION}" = "${EXPECTED_VERSION}" ]; then
echo " ${BINARY_NAME} ${EXPECTED_VERSION} installed successfully"
else
echo " Warning: installed version (${INSTALLED_VERSION}) does not match expected version (${EXPECTED_VERSION})"
fi
}
104 changes: 91 additions & 13 deletions stackit/internal/services/dns/recordset/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package dns

import (
"context"
"errors"
"fmt"
"net/http"
"strings"

"github.com/hashicorp/terraform-plugin-framework-validators/int64validator"
Expand All @@ -16,6 +18,7 @@ import (
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/stackitcloud/stackit-sdk-go/core/oapierror"
"github.com/stackitcloud/stackit-sdk-go/services/dns"
"github.com/stackitcloud/stackit-sdk-go/services/dns/wait"
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion"
Expand Down Expand Up @@ -200,6 +203,13 @@ func (r *recordSetResource) Create(ctx context.Context, req resource.CreateReque
return
}

// Get a fresh copy from plan for minimal state
var minimalModel Model
resp.Diagnostics.Append(req.Plan.Get(ctx, &minimalModel)...)
if resp.Diagnostics.HasError() {
return
}

projectId := model.ProjectId.ValueString()
zoneId := model.ZoneId.ValueString()
ctx = tflog.SetField(ctx, "project_id", projectId)
Expand All @@ -219,18 +229,36 @@ func (r *recordSetResource) Create(ctx context.Context, req resource.CreateReque
}

// Write id attributes to state before polling via the wait handler - just in case anything goes wrong during the wait handler
utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{
"project_id": projectId,
"zone_id": zoneId,
"record_set_id": *recordSetResp.Rrset.Id,
})
recordSetId := *recordSetResp.Rrset.Id
minimalModel.RecordSetId = types.StringValue(recordSetId)
minimalModel.Id = utils.BuildInternalTerraformId(projectId, zoneId, recordSetId)

// Set all unknown/null fields to null before saving state
if err := utils.SetModelFieldsToNull(ctx, &minimalModel); err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating record set", fmt.Sprintf("Setting model fields to null: %v", err))
return
}

diags = resp.State.Set(ctx, minimalModel)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}

if !utils.ShouldWait() {
tflog.Info(ctx, "Skipping wait; async mode for Crossplane/Upjet")
return
}

waitResp, err := wait.CreateRecordSetWaitHandler(ctx, r.client, projectId, zoneId, *recordSetResp.Rrset.Id).WaitWithContext(ctx)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating record set", fmt.Sprintf("Instance creation waiting: %v", err))
if utils.ShouldIgnoreWaitError(err) {
tflog.Warn(ctx, fmt.Sprintf("Record set creation waiting failed: %v. The record set creation was triggered but waiting for completion was interrupted. The record set may still be creating.", err))
return
}

core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating record set", fmt.Sprintf("Record set creation waiting: %v", err))

return
}

Expand Down Expand Up @@ -266,6 +294,12 @@ func (r *recordSetResource) Read(ctx context.Context, req resource.ReadRequest,

recordSetResp, err := r.client.GetRecordSet(ctx, projectId, zoneId, recordSetId).Execute()
if err != nil {
var oapiErr *oapierror.GenericOpenAPIError
ok := errors.As(err, &oapiErr)
if ok && (oapiErr.StatusCode == http.StatusNotFound || oapiErr.StatusCode == http.StatusGone) {
resp.State.RemoveResource(ctx)
return
}
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading record set", fmt.Sprintf("Calling API: %v", err))
return
}
Expand Down Expand Up @@ -319,9 +353,21 @@ func (r *recordSetResource) Update(ctx context.Context, req resource.UpdateReque
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating record set", err.Error())
return
}

if !utils.ShouldWait() {
if utils.ShouldIgnoreWaitError(err) {
tflog.Warn(ctx, fmt.Sprintf("Record set update waiting failed: %v. The record set update was triggered but waiting for completion was interrupted. The record set may still be updating.", err))
return
}

core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating record set", fmt.Sprintf("Record set update waiting: %v", err))

return
}

waitResp, err := wait.PartialUpdateRecordSetWaitHandler(ctx, r.client, projectId, zoneId, recordSetId).WaitWithContext(ctx)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating record set", fmt.Sprintf("Instance update waiting: %v", err))
tflog.Warn(ctx, fmt.Sprintf("Record set update waiting failed: %v. The record set update was triggered but waiting for completion was interrupted. The record set may still be updating.", err))
return
}

Expand Down Expand Up @@ -358,11 +404,31 @@ func (r *recordSetResource) Delete(ctx context.Context, req resource.DeleteReque
// Delete existing record set
_, err := r.client.DeleteRecordSet(ctx, projectId, zoneId, recordSetId).Execute()
if err != nil {
// If resource is already gone (404 or 410), treat as success for idempotency
var oapiErr *oapierror.GenericOpenAPIError
ok := errors.As(err, &oapiErr)
if ok && (oapiErr.StatusCode == http.StatusNotFound || oapiErr.StatusCode == http.StatusGone) {
tflog.Info(ctx, "Record set already deleted")
return
}
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting record set", fmt.Sprintf("Calling API: %v", err))
return
}

if !utils.ShouldWait() {
tflog.Info(ctx, "Skipping wait; async mode for Crossplane/Upjet")
return
}

_, err = wait.DeleteRecordSetWaitHandler(ctx, r.client, projectId, zoneId, recordSetId).WaitWithContext(ctx)
if err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting record set", fmt.Sprintf("Instance deletion waiting: %v", err))
if utils.ShouldIgnoreWaitError(err) {
tflog.Warn(ctx, fmt.Sprintf("Record set deletion waiting failed: %v. The record set deletion was triggered but waiting for completion was interrupted. The record set may still be deleting.", err))
return
}

core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting record set", fmt.Sprintf("Record set deletion waiting: %v", err))

return
}
tflog.Info(ctx, "DNS record set deleted")
Expand All @@ -380,11 +446,23 @@ func (r *recordSetResource) ImportState(ctx context.Context, req resource.Import
return
}

utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]interface{}{
"project_id": idParts[0],
"zone_id": idParts[1],
"record_set_id": idParts[2],
})
var model Model
model.ProjectId = types.StringValue(idParts[0])
model.ZoneId = types.StringValue(idParts[1])
model.RecordSetId = types.StringValue(idParts[2])
model.Id = utils.BuildInternalTerraformId(idParts[0], idParts[1], idParts[2])

if err := utils.SetModelFieldsToNull(ctx, &model); err != nil {
core.LogAndAddError(ctx, &resp.Diagnostics, "Error importing zone", fmt.Sprintf("Setting model fields to null: %v", err))
return
}

diags := resp.State.Set(ctx, model)
resp.Diagnostics.Append(diags...)
if diags.HasError() {
return
}

tflog.Info(ctx, "DNS record set state imported")
}

Expand Down
Loading
Loading