Skip to content
Merged
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
21 changes: 21 additions & 0 deletions docs/getting-started.de.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,27 @@ Diese Anleitung führt Sie durch die Bereitstellung der STACKIT Landing Zone von
> Die Module in diesem Terraform-Repository sind nicht zur direkten Nutzung vorgesehen. Klonen Sie das Repository und passen Sie die Module an Ihre konkreten Anforderungen an. Es gibt keine Garantien für Migrationspfade oder dafür, dass die Module unverändert bleiben.

<a id="prerequisites"></a>
## Upgrade von Bucket-Namen und Observability-Einstellungen

Bucket-Namen gelten STACKIT-weit. Der Standard `bucket_naming = "project-id"`
enthält jetzt die vollständige ID des jeweiligen Management- oder Anwendungsprojekts.
Nur der lesbare Präfix wird auf insgesamt höchstens 63 Zeichen gekürzt.
Die State-Adressen bleiben stabil, Bucket-Namen sind jedoch unveränderlich:
**Vor dem Upgrade einer Installation mit vorhandenen bisherigen Bucket-Namen
`bucket_naming = "legacy"` setzen**, sofern keine separate Ersetzung bzw.
Daten-/Backend-Migration geprüft wurde. Es gibt keine automatische Datenmigration
oder State-Neuanlage. Ein teilweise ausgeführtes Deployment ohne Buckets kann
Projekt und Checkpoint mit dem neuen Standard beibehalten. Vor Apply einen neuen
vollständigen Plan prüfen.
Siehe [Issue #127](https://github.com/stackitcloud/stackit-landing-zone/issues/127).

Zentrale Observability-Aufbewahrungswerte sind optional/null und verwenden sonst
die Servicestandards. Monitoring unterstützt keine Logs-/Trace-Aufbewahrung;
Frontend und MetricsEndpoint keine Aufbewahrungsüberschreibungen. Explizite Werte
müssen innerhalb der Servicegrenzen liegen (ganze Tage: Logs/Traces 1–30,
Metriken/Downsampling 1–780). Das native Root weist diese unzulässigen Kombinationen
vor der Ressourcenbereitstellung zurück.

## Voraussetzungen

- Eine **STACKIT-Organisation**, in der Ihr Benutzerkonto registriert ist
Expand Down
19 changes: 19 additions & 0 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,25 @@ This guide walks you through deploying the STACKIT Landing Zone from scratch.
> [!NOTE]
> The modules in this Terraform repository are not intended to be used directly. Clone the repository and adjust the modules to your specific requirements. No guarantees are made about migration paths or that the modules will remain unchanged.

## Upgrading bucket naming and Observability settings

Bucket names are STACKIT-wide. The default `bucket_naming = "project-id"` now
includes the complete owning project UUID for management and application buckets.
Only the readable prefix is truncated to keep the full name within 63 characters.
Existing bucket resource addresses remain stable, but names are immutable:
**set `bucket_naming = "legacy"` before upgrading an installation that already
owns legacy-named buckets**, unless a separate replacement/data/backend migration
has been reviewed. No data migration or state reset happens automatically.
A partially applied deployment without buckets can keep its original project and
checkpoint and use the new default. Review a fresh full Plan before Apply.
See [issue #127](https://github.com/stackitcloud/stackit-landing-zone/issues/127).

Central Observability retention overrides are optional/null and otherwise use
service defaults. Monitoring supports no logs/traces retention; Frontend and
MetricsEndpoint support no retention overrides. Explicit values must fit the
service limits (whole days: logs/traces 1–30, metrics/downsampling 1–780). The native
root rejects these incompatible combinations before provisioning resources.

## Prerequisites

- A **STACKIT organization** with your user account registered
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -1346,7 +1346,7 @@ export class AssistantModelServing {
[
{
role: "system",
content: `Translate the CURRENT user task into reviewable native Editor settings using ONLY the supplied closed schemas. Reply in the user's language. This Editor contract supersedes historical assistant claims that audit/VPN/platform/projects are unsupported. Firewall policy, secrets, identity binding, publication and execution are excluded. Never generate arbitrary JSON/HCL, cloud IDs, credentials or verified identities. Actual user messages/current saved values/available catalogue entries are the only sources for addresses, emails, IDs and product selections. Never fabricate availability, performance, quotas, compliance or event delivery.
content: `Translate the CURRENT user task into reviewable native Editor settings using ONLY the supplied closed schemas. Reply in the user's language. This Editor contract supersedes historical assistant claims that audit/VPN/platform/projects are unsupported. Firewall policy, secrets, identity binding, publication and execution are excluded. Never generate arbitrary JSON/HCL, cloud IDs, credentials or verified identities. Actual user messages/current saved values/available catalogue entries are the only sources for addresses, emails, IDs and product selections. Never fabricate availability, performance, quotas, compliance or event delivery. Respect supplied service-plan capability flags: false forbids an option, absent means unknown. Use the reviewed static capability/retention policy only as an explicitly identified fallback; never silently change a plan or discard incompatible saved values.
Choose at most three clear business questions when essential information is missing, with up to six meaningful options. ACL questions use free text and no invented network examples or selectable CIDRs: ask the user for their actual access networks. A setup request without essential choices returns questions only and changes=[]; never propose an incomplete new component. Reuse actual user answers, not assistant suggestions; do not repeat old firewall questions after a topic switch. Explain concrete choices briefly, not a dump of every field. A reply containing only values continues the language of the latest natural-language USER turn; technical field names do not imply English.
Operations: merge recursively changes only supplied object/map attributes; arrays and null are atomic replacements. replace replaces the COMPLETE value at that path. For projectTemplates and DNS zone collections each changed/removed entry is separately reviewable. A USER-defined desired complete set is replacement intent even without the word delete; explicitly additional entries are merge, corrections update existing stable keys. Follow the source-grounded templateSetup.collectionMode and dnsCollection, never a blanket keep/delete-examples rule. Ask when intent is ambiguous. default removes an explicit optional override/entry and restores the native default, value=null. Never use replace/default without user intent. Never reorder lists unless requested. Each path appears only once; never emit overlapping parent/child paths. Do not repeat unchanged settings. Use an existing nested path to reset/delete just that setting, preserving unrelated components. New collection entries use their parent path and a complete native value with existing stable keys retained.
Audit creation needs explicit retention_days, ACL CIDRs and s3_object_lock together; ask for missing decisions, do not activate a public ACL or irreversible Object Lock silently. Offer explained Object Lock yes/no options; native default is true but contradictory documentation is not consent. Retention with lock must be 1-365 days. For organizational/default scope OMIT link_scopes or use null: NEVER [], and never copy the saved organization UUID into a new scope override. Non-null link_scopes are only for explicitly requested custom per-resource scope overrides, never invented tenant/project scope IDs.
Expand Down
107 changes: 103 additions & 4 deletions landing-zone-configurator/app/apps/api/src/assistant/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ import {
compileCommonConfiguration,
effectiveInput,
isCommonDraft,
type ObservabilityPlanCatalogue,
objectValue,
observabilityRetentionFindings,
readEditorDraft,
} from "@lzc/domain";
import type pg from "pg";
Expand Down Expand Up @@ -314,6 +316,76 @@ export class AssistantConversations {
Partial<Pick<PostgresCloudCatalogues, "pim">>,
) {}

private async loadObservabilityCatalogue(
session: Session,
id: string,
localDocument?: unknown,
changes?: { path: string }[],
): Promise<ObservabilityPlanCatalogue | undefined> {
const affectsObservability = (items: { path: string }[]) =>
items.some(
(change) =>
change.path === "observability" ||
change.path.startsWith("observability."),
);
if (!this.catalogues || (changes && !affectsObservability(changes)))
return undefined;
const region = await this.work(
session,
id,
async (client, configuration) => {
if (!changes) {
const row = (
await client.query(
"SELECT proposal FROM lzc.assistant_conversations WHERE configuration_id=$1",
[id],
)
).rows[0];
const proposal = z
.object({ changes: z.array(z.object({ path: z.string() })) })
.safeParse(row?.proposal);
if (!proposal.success || !affectsObservability(proposal.data.changes))
return undefined;
}
const draft = readEditorDraft(localDocument ?? configuration.document);
if (!isCommonDraft(draft)) return undefined;
return compileCommonConfiguration(draft).region === "eu02"
? ("eu02" as const)
: ("eu01" as const);
},
);
if (!region) return undefined;
// Credential/network reads finish before opening the proposal write transaction.
return this.catalogues.automatic(session, region).catch(() => undefined);
}

private checkObservabilitySelection(
draft: Parameters<typeof compileCommonConfiguration>[0],
changes: { path: string }[],
catalogue?: ObservabilityPlanCatalogue,
) {
if (
!changes.some(
(change) =>
change.path === "observability" ||
change.path.startsWith("observability."),
)
)
return;
const values = compileCommonConfiguration(draft);
if (values.observability == null) return;
const region = values.region === "eu02" ? "eu02" : "eu01";
const report = observabilityRetentionFindings(
objectValue(values.observability),
catalogue?.region === region ? catalogue : undefined,
);
if (report.findings.length)
throw new AssistantError(
422,
"assistant_observability_plan_incompatible",
);
}

private async productCatalogues(
session: Session,
regions: ("eu01" | "eu02")[],
Expand Down Expand Up @@ -378,7 +450,11 @@ export class AssistantConversations {
part?.status === "available"
? part.options
.slice(0, 50)
.map(({ value, label }) => ({ value, label }))
.map(({ value, label, capabilities }) => ({
value,
label,
...(capabilities ? { capabilities } : {}),
}))
: [],
totalOptions: part?.options.length ?? 0,
truncated: (part?.options.length ?? 0) > 50,
Expand Down Expand Up @@ -665,6 +741,13 @@ export class AssistantConversations {
knowledge.rules.map((rule) => rule.id),
);
stream?.signal?.throwIfAborted();
const catalogue = await this.loadObservabilityCatalogue(
session,
id,
undefined,
proposal.changes,
);
stream?.signal?.throwIfAborted();
await this.work(session, id, async (_client, configuration) => {
if (configuration.revision !== revision)
throw new AssistantError(409, "configuration_changed");
Expand All @@ -675,7 +758,13 @@ export class AssistantConversations {
"assistant_common_configuration_required",
);
try {
applyAssistantChanges(draft, proposal.changes);
const candidate = applyAssistantChanges(draft, proposal.changes);
if (isCommonDraft(candidate))
this.checkObservabilitySelection(
candidate,
proposal.changes,
catalogue,
);
} catch (error) {
if (error instanceof z.ZodError)
throw new AssistantInferenceError("invalid_scope");
Expand Down Expand Up @@ -728,7 +817,7 @@ export class AssistantConversations {
}
}

preview(
async preview(
session: Session,
id: string,
conversationRevision: number,
Expand All @@ -738,6 +827,11 @@ export class AssistantConversations {
baselineDocument: unknown,
confirmedRevision?: number,
) {
const catalogue = await this.loadObservabilityCatalogue(
session,
id,
localDocument,
);
return this.work(session, id, async (client, configuration) => {
const row = (
await client.query<Conversation>(
Expand Down Expand Up @@ -845,6 +939,8 @@ export class AssistantConversations {
let candidate: ReturnType<typeof applyAssistantChanges>;
try {
candidate = applyAssistantChanges(draft, selection);
if (isCommonDraft(candidate))
this.checkObservabilitySelection(candidate, selection, catalogue);
} catch (error) {
if (
error instanceof z.ZodError ||
Expand Down Expand Up @@ -915,13 +1011,14 @@ export class AssistantConversations {
});
}

accept(
async accept(
session: Session,
id: string,
conversationRevision: number,
baseRevision: number,
selectedFields?: string[],
) {
const catalogue = await this.loadObservabilityCatalogue(session, id);
return this.work(session, id, async (client, configuration) => {
const row = (
await client.query<Conversation>(
Expand Down Expand Up @@ -976,6 +1073,8 @@ export class AssistantConversations {
let candidate: ReturnType<typeof applyAssistantChanges>;
try {
candidate = applyAssistantChanges(draft, changes);
if (isCommonDraft(candidate))
this.checkObservabilitySelection(candidate, changes, catalogue);
} catch (error) {
if (
selectedFields !== undefined &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export const assistantTopicFields: Record<
"organization_auditors",
],
},
"management-state": { paths: [], prefixes: [] },
"management-state": { paths: [], prefixes: ["bucket_naming"] },
observability: { paths: [], prefixes: ["observability"] },
"audit-logs": { paths: [], prefixes: ["audit_logs"] },
"federated-identities": {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { ObservabilityPlanCapabilities } from "@lzc/domain";
import type pg from "pg";
import { z } from "zod";
import type { Session } from "../auth/store.js";
Expand All @@ -15,7 +16,11 @@ export const catalogueRequest = z
})
.strict();
export type CatalogueRequest = z.infer<typeof catalogueRequest>;
export type CatalogueOption = { value: string; label: string };
export type CatalogueOption = {
value: string;
label: string;
capabilities?: ObservabilityPlanCapabilities;
};
export type CataloguePart = {
status: "available" | "unavailable";
options: CatalogueOption[];
Expand Down Expand Up @@ -107,7 +112,16 @@ const ske = z.object({
volumeTypes: names,
});
const observability = z.object({
plans: z.array(z.object({ name: text.optional() })).max(2000),
plans: z
.array(
z.object({
name: text.optional(),
totalMetricSamples: z.number().int().nonnegative().optional(),
logsStorage: z.number().int().nonnegative().optional(),
tracesStorage: z.number().int().nonnegative().optional(),
}),
)
.max(2000),
});
const iaasMachines = z.object({ items: names });
const iaasImages = z.object({
Expand Down Expand Up @@ -477,7 +491,31 @@ export class StackitCatalogueClient {
if (observabilityResult.status === "fulfilled")
result.observabilityPlans = available(
observabilityResult.value.plans.flatMap((v) =>
v.name ? [{ value: v.name, label: v.name }] : [],
v.name
? [
{
value: v.name,
label: v.name,
...(v.totalMetricSamples !== undefined ||
v.logsStorage !== undefined ||
v.tracesStorage !== undefined
? {
capabilities: {
...(v.totalMetricSamples === undefined
? {}
: { metricsRetention: v.totalMetricSamples > 0 }),
...(v.logsStorage === undefined
? {}
: { logsRetention: v.logsStorage > 0 }),
...(v.tracesStorage === undefined
? {}
: { tracesRetention: v.tracesStorage > 0 }),
},
}
: {}),
},
]
: [],
),
);
if (machinesResult.status === "fulfilled")
Expand Down
Loading
Loading