Compare commits

..

8 Commits

Author SHA1 Message Date
Fabiano Fidêncio
8950f1caeb Merge pull request #12706 from fidencio/topic/ci-tdx-nydus-snapshotter
tests: Use the helm chart to setup nydus for TDX
2026-03-24 22:37:38 +01:00
Fabiano Fidêncio
814ae53d77 tests: Use the helm chart to setup nydus for TDX
Now that containerd 2.3.0-beta.0 has been released, it brings fixes for
multi-snapshotters that allows us to test the baremetal machines in the
same way we test the non-baremetal ones.

Let's start doing the switch for TDX as timezone is friendlier with
Mikko.

Signed-off-by: Fabiano Fidêncio <ffidencio@nvidia.com>
2026-03-24 19:13:59 +01:00
Fabiano Fidêncio
27dfb0d06f Merge pull request #12724 from fidencio/topic/kata-deploy-properly-cleanup-nydus-snapshotter-on-uninstall
kata-deploy: nydus: clean containerd metadata before cleaning up the backend
2026-03-24 19:13:25 +01:00
Fabiano Fidêncio
fd583d833b kata-deploy: nydus: clean containerd metadata before wiping backend
When /var/lib/nydus-snapshotter is removed, containerd's BoltDB
(meta.db at /var/lib/containerd/) still holds snapshot records for
the nydus snapshotter.  On the next install these stale records cause
image pulls to fail with:

  "unable to prepare extraction snapshot:
   target snapshot \"sha256:...\": already exists"

The failure path in core/unpack/unpacker.go:
1. sn.Prepare() → metadata layer finds the target chainID in BoltDB
   → returns AlreadyExists without touching the nydus backend.
2. sn.Stat()    → metadata layer finds the BoltDB record, then calls
   s.Snapshotter.Stat(bkey) on the nydus gRPC backend → NotFound
   (backend was wiped).
3. The unpacker treats NotFound as a transient key-collision race and
   retries 3 times; all 3 attempts hit the same dead end, and the
   pull is aborted.

The commit message of 62ad0814c ("nydus: Always start from a clean
state") assumed "containerd will re-pull/re-unpack when it finds non-
existent snapshots", but that is not what happens: the metadata layer
intercepts the Prepare call in BoltDB before the backend is ever
consulted.

Fix: call cleanup_containerd_nydus_snapshots() before stopping the
nydus service (and thus before wiping its data directory) in both
install_nydus_snapshotter and uninstall_nydus_snapshotter.

The cleanup must run while the service is still up because ctr
snapshots rm goes through the metadata layer which calls the nydus
gRPC backend to physically remove the snapshot; if the service is
already stopped the backend call fails and the BoltDB record remains.

The cleanup:
- Discovers all containerd namespaces via `ctr namespaces ls -q`
  (falls back to k8s.io if that fails).
- Removes containers whose Snapshotter field matches the nydus plugin
  name; these become dangling references once snapshots are gone and
  can confuse container reconciliation after an aborted CI run.
- Removes snapshots round by round (leaf-first) until either the list
  is empty or no progress can be made (see below).

Note: containerd's GC cannot substitute for this explicit cleanup.
The image record (a GC root) references content blobs which reference
the snapshots via gc.ref labels, keeping the entire chain alive in
the GC graph even after the nydus backend is wiped.

Snapshot removal rounds
-----------------------
Snapshot chains are linear: an image with N layers produces a chain
of N snapshots, each parented on the previous.  Only the current leaf
can be removed each round, so N layers require exactly N rounds.
There is no fixed round cap — the loop terminates when either the
list reaches zero (success) or a round removes nothing at all
(all remaining snapshots are actively in use by running workloads).

Active workload safety
----------------------
If active workloads still hold nydus snapshots (e.g. during a live
upgrade), no progress is made in a round and cleanup_nydus_snapshots
returns false.  Both install_nydus_snapshotter and
uninstall_nydus_snapshotter gate the fs::remove_dir_all on that
return value:

  - true  → proceed as before: stop service, wipe data dir.
  - false → stop service, skip data dir removal, log a warning.
            The new nydus instance starts on the existing backend
            state; running containers are left intact.

Signed-off-by: Fabiano Fidêncio <ffidencio@nvidia.com>
Made-with: Cursor
2026-03-24 16:44:25 +01:00
Fabiano Fidêncio
eb4ce0e98b Merge pull request #12676 from manuelh-dev/mahuber/gpu-ci-data-storage
tests: gpu: use container data storage feature
2026-03-24 09:59:13 +01:00
Manuel Huber
79efe3e041 tests: gpu: use container data storage feature
Use the container data storage feature for the k8s-nvidia-nim.bats
test pod manifests. This reduces the pods' memory requirements.
For this, enable the block-encrypted emptydir_mode for the NVIDIA
GPU TEE handlers.

Signed-off-by: Manuel Huber <manuelh@nvidia.com>
2026-03-23 11:43:11 -07:00
Steve Horsman
2728b493d5 Merge pull request #12681 from manuelh-dev/mahuber/ci-pip-py-venv
tests: cc: setup function for python venv
2026-03-23 14:33:30 +00:00
Manuel Huber
5765bc97b4 tests: cc: setup function for python venv
We recently had a failure on a new CI runner where
${HOME}/.cicd/venv/bin/activate was not present. The relevant call
originated from ensure_sev_snp_measure. Thus, add a function
ensure_cicd_python_venv before callers to pip install.
Currently, the NVIDIA NIM test and the confidential attestation
tests use pip to install dependencies.

Signed-off-by: Manuel Huber <manuelh@nvidia.com>
2026-03-18 17:07:47 -07:00
27 changed files with 624 additions and 543 deletions

View File

@@ -727,7 +727,7 @@ disable_guest_empty_dir = @DEFDISABLEGUESTEMPTYDIR@
# - block-encrypted
# Plugs a block device to be encrypted in the guest.
#
emptydir_mode = "@DEFEMPTYDIRMODE@"
emptydir_mode = "@DEFEMPTYDIRMODE_COCO@"
# Enabled experimental feature list, format: ["a", "b"].
# Experimental features are features not stable enough for production,

View File

@@ -704,7 +704,7 @@ disable_guest_empty_dir = @DEFDISABLEGUESTEMPTYDIR@
# - block-encrypted
# Plugs a block device to be encrypted in the guest.
#
emptydir_mode = "@DEFEMPTYDIRMODE@"
emptydir_mode = "@DEFEMPTYDIRMODE_COCO@"
# Enabled experimental feature list, format: ["a", "b"].
# Experimental features are features not stable enough for production,

View File

@@ -26,7 +26,7 @@ require (
github.com/docker/go-units v0.5.0
github.com/fsnotify/fsnotify v1.9.0
github.com/go-ini/ini v1.67.0
github.com/go-openapi/errors v0.22.7
github.com/go-openapi/errors v0.22.1
github.com/go-openapi/runtime v0.28.0
github.com/go-openapi/strfmt v0.23.0
github.com/go-openapi/swag v0.23.1

View File

@@ -107,8 +107,8 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/analysis v0.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC03zFCU=
github.com/go-openapi/analysis v0.23.0/go.mod h1:9mz9ZWaSlV8TvjQHLl2mUW2PbZtemkE8yA5v22ohupo=
github.com/go-openapi/errors v0.22.7 h1:JLFBGC0Apwdzw3484MmBqspjPbwa2SHvpDm0u5aGhUA=
github.com/go-openapi/errors v0.22.7/go.mod h1://QW6SD9OsWtH6gHllUCddOXDL0tk0ZGNYHwsw4sW3w=
github.com/go-openapi/errors v0.22.1 h1:kslMRRnK7NCb/CvR1q1VWuEQCEIsBGn5GgKD9e+HYhU=
github.com/go-openapi/errors v0.22.1/go.mod h1:+n/5UdIqdVnLIJ6Q9Se8HNGUXYaY6CN8ImWzfi/Gzp0=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
@@ -123,8 +123,6 @@ github.com/go-openapi/strfmt v0.23.0 h1:nlUS6BCqcnAk0pyhi9Y+kdDVZdZMHfEKQiS4HaMg
github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4=
github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU=
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
github.com/go-openapi/testify/v2 v2.4.0 h1:8nsPrHVCWkQ4p8h1EsRVymA2XABB4OT40gcvAu+voFM=
github.com/go-openapi/testify/v2 v2.4.0/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58=
github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ=
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=

View File

@@ -1,26 +0,0 @@
# top-most EditorConfig file
root = true
# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
# Set default charset
[*.{js,py,go,scala,rb,java,html,css,less,sass,md}]
charset = utf-8
# Tab indentation (no size specified)
[*.go]
indent_style = tab
[*.md]
trim_trailing_whitespace = false
# Matches the exact files either package.json or .travis.yml
[{package.json,.travis.yml}]
indent_style = space
indent_size = 2

View File

@@ -1,7 +1,2 @@
*.out
*.cov
.idea
.env
.mcp.json
.claude/
settings.local.json
secrets.yml
coverage.out

View File

@@ -1,71 +1,55 @@
version: "2"
linters-settings:
gocyclo:
min-complexity: 45
dupl:
threshold: 200
goconst:
min-len: 2
min-occurrences: 3
linters:
default: all
enable-all: true
disable:
- depguard
- unparam
- lll
- gochecknoinits
- gochecknoglobals
- funlen
- godox
- exhaustruct
- nlreturn
- nonamedreturns
- noinlineerr
- paralleltest
- recvcheck
- testpackage
- thelper
- tparallel
- varnamelen
- gocognit
- whitespace
- wrapcheck
- wsl
- wsl_v5
settings:
dupl:
threshold: 200
goconst:
min-len: 2
min-occurrences: 3
cyclop:
max-complexity: 20
gocyclo:
min-complexity: 20
exhaustive:
default-signifies-exhaustive: true
default-case-required: true
lll:
line-length: 180
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
rules:
- linters:
- revive
text: "avoid package names that conflict with Go standard library package names"
paths:
- third_party$
- builtin$
- examples$
formatters:
enable:
- gofmt
- goimports
- wrapcheck
- testpackage
- nlreturn
- errorlint
- nestif
- godot
- gofumpt
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
issues:
# Maximum issues count per one linter.
# Set to 0 to disable.
# Default: 50
max-issues-per-linter: 0
# Maximum count of issues with the same text.
# Set to 0 to disable.
# Default: 3
max-same-issues: 0
- paralleltest
- tparallel
- thelper
- exhaustruct
- varnamelen
- gci
- depguard
- errchkjson
- inamedparam
- nonamedreturns
- musttag
- ireturn
- forcetypeassert
- cyclop
# deprecated linters
#- deadcode
#- interfacer
#- scopelint
#- varcheck
#- structcheck
#- golint
#- nosnakecase
#- maligned
#- goerr113
#- ifshort
#- gomnd
#- exhaustivestruct

View File

@@ -23,9 +23,7 @@ include:
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
@@ -57,7 +55,7 @@ further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at <ivan+abuse@flanders.co.nz>. All
reported by contacting the project team at ivan+abuse@flanders.co.nz. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
@@ -70,7 +68,7 @@ members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at [<http://contributor-covenant.org/version/1/4>][version]
available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/

View File

@@ -1,25 +0,0 @@
# Contributors
- Repository: ['go-openapi/errors']
| Total Contributors | Total Contributions |
| --- | --- |
| 13 | 110 |
| Username | All Time Contribution Count | All Commits |
| --- | --- | --- |
| @casualjim | 58 | <https://github.com/go-openapi/errors/commits?author=casualjim> |
| @fredbi | 36 | <https://github.com/go-openapi/errors/commits?author=fredbi> |
| @youyuanwu | 5 | <https://github.com/go-openapi/errors/commits?author=youyuanwu> |
| @alexandear | 2 | <https://github.com/go-openapi/errors/commits?author=alexandear> |
| @fiorix | 1 | <https://github.com/go-openapi/errors/commits?author=fiorix> |
| @ligustah | 1 | <https://github.com/go-openapi/errors/commits?author=ligustah> |
| @artemseleznev | 1 | <https://github.com/go-openapi/errors/commits?author=artemseleznev> |
| @gautierdelorme | 1 | <https://github.com/go-openapi/errors/commits?author=gautierdelorme> |
| @guillemj | 1 | <https://github.com/go-openapi/errors/commits?author=guillemj> |
| @maxatome | 1 | <https://github.com/go-openapi/errors/commits?author=maxatome> |
| @Simon-Li | 1 | <https://github.com/go-openapi/errors/commits?author=Simon-Li> |
| @aokumasan | 1 | <https://github.com/go-openapi/errors/commits?author=aokumasan> |
| @ujjwalsh | 1 | <https://github.com/go-openapi/errors/commits?author=ujjwalsh> |
_this file was generated by the [Contributors GitHub Action](https://github.com/github/contributors)_

View File

@@ -1,118 +1,8 @@
# errors
# OpenAPI errors [![Build Status](https://github.com/go-openapi/errors/actions/workflows/go-test.yml/badge.svg)](https://github.com/go-openapi/errors/actions?query=workflow%3A"go+test") [![codecov](https://codecov.io/gh/go-openapi/errors/branch/master/graph/badge.svg)](https://codecov.io/gh/go-openapi/errors)
<!-- Badges: status -->
[![Tests][test-badge]][test-url] [![Coverage][cov-badge]][cov-url] [![CI vuln scan][vuln-scan-badge]][vuln-scan-url] [![CodeQL][codeql-badge]][codeql-url]
<!-- Badges: release & docker images -->
<!-- Badges: code quality -->
<!-- Badges: license & compliance -->
[![Release][release-badge]][release-url] [![Go Report Card][gocard-badge]][gocard-url] [![CodeFactor Grade][codefactor-badge]][codefactor-url] [![License][license-badge]][license-url]
<!-- Badges: documentation & support -->
<!-- Badges: others & stats -->
[![GoDoc][godoc-badge]][godoc-url] [![Discord Channel][discord-badge]][discord-url] [![go version][goversion-badge]][goversion-url] ![Top language][top-badge] ![Commits since latest release][commits-badge]
---
[![Slack Status](https://slackin.goswagger.io/badge.svg)](https://slackin.goswagger.io)
[![license](http://img.shields.io/badge/license-Apache%20v2-orange.svg)](https://raw.githubusercontent.com/go-openapi/errors/master/LICENSE)
[![Go Reference](https://pkg.go.dev/badge/github.com/go-openapi/errors.svg)](https://pkg.go.dev/github.com/go-openapi/errors)
[![Go Report Card](https://goreportcard.com/badge/github.com/go-openapi/errors)](https://goreportcard.com/report/github.com/go-openapi/errors)
Shared errors and error interface used throughout the various libraries found in the go-openapi toolkit.
## Announcements
* **2025-12-19** : new community chat on discord
* a new discord community channel is available to be notified of changes and support users
* our venerable Slack channel remains open, and will be eventually discontinued on **2026-03-31**
You may join the discord community by clicking the invite link on the discord badge (also above). [![Discord Channel][discord-badge]][discord-url]
Or join our Slack channel: [![Slack Channel][slack-logo]![slack-badge]][slack-url]
## Status
API is stable.
## Import this library in your project
```cmd
go get github.com/go-openapi/errors
```
## Basic usage
```go
const url = "https://www.example.com/#"
errGeneric := New(401,"onvalid argument: %s", url)
errNotFound := NotFound("resource not found: %s", url)
errNotImplemented := NotImplemented("method: %s", url)
```
## Change log
See <https://github.com/go-openapi/errors/releases>
<!--
## References
-->
## Licensing
This library ships under the [SPDX-License-Identifier: Apache-2.0](./LICENSE).
<!--
## Limitations
-->
## Other documentation
* [All-time contributors](./CONTRIBUTORS.md)
* [Contributing guidelines](.github/CONTRIBUTING.md)
* [Maintainers documentation](docs/MAINTAINERS.md)
* [Code style](docs/STYLE.md)
## Cutting a new release
Maintainers can cut a new release by either:
* running [this workflow](https://github.com/go-openapi/errors/actions/workflows/bump-release.yml)
* or pushing a semver tag
* signed tags are preferred
* The tag message is prepended to release notes
<!-- Badges: status -->
[test-badge]: https://github.com/go-openapi/errors/actions/workflows/go-test.yml/badge.svg
[test-url]: https://github.com/go-openapi/errors/actions/workflows/go-test.yml
[cov-badge]: https://codecov.io/gh/go-openapi/errors/branch/master/graph/badge.svg
[cov-url]: https://codecov.io/gh/go-openapi/errors
[vuln-scan-badge]: https://github.com/go-openapi/errors/actions/workflows/scanner.yml/badge.svg
[vuln-scan-url]: https://github.com/go-openapi/errors/actions/workflows/scanner.yml
[codeql-badge]: https://github.com/go-openapi/errors/actions/workflows/codeql.yml/badge.svg
[codeql-url]: https://github.com/go-openapi/errors/actions/workflows/codeql.yml
<!-- Badges: release & docker images -->
[release-badge]: https://badge.fury.io/gh/go-openapi%2Ferrors.svg
[release-url]: https://badge.fury.io/gh/go-openapi%2Ferrors
<!-- Badges: code quality -->
[gocard-badge]: https://goreportcard.com/badge/github.com/go-openapi/errors
[gocard-url]: https://goreportcard.com/report/github.com/go-openapi/errors
[codefactor-badge]: https://img.shields.io/codefactor/grade/github/go-openapi/errors
[codefactor-url]: https://www.codefactor.io/repository/github/go-openapi/errors
<!-- Badges: documentation & support -->
[godoc-badge]: https://pkg.go.dev/badge/github.com/go-openapi/errors
[godoc-url]: http://pkg.go.dev/github.com/go-openapi/errors
[slack-logo]: https://a.slack-edge.com/e6a93c1/img/icons/favicon-32.png
[slack-badge]: https://img.shields.io/badge/slack-blue?link=https%3A%2F%2Fgoswagger.slack.com%2Farchives%2FC04R30YM
[slack-url]: https://goswagger.slack.com/archives/C04R30YMU
[discord-badge]: https://img.shields.io/discord/1446918742398341256?logo=discord&label=discord&color=blue
[discord-url]: https://discord.gg/twZ9BwT3
<!-- Badges: license & compliance -->
[license-badge]: http://img.shields.io/badge/license-Apache%20v2-orange.svg
[license-url]: https://github.com/go-openapi/errors/?tab=Apache-2.0-1-ov-file#readme
<!-- Badges: others & stats -->
[goversion-badge]: https://img.shields.io/github/go-mod/go-version/go-openapi/errors
[goversion-url]: https://github.com/go-openapi/errors/blob/master/go.mod
[top-badge]: https://img.shields.io/github/languages/top/go-openapi/errors
[commits-badge]: https://img.shields.io/github/commits-since/go-openapi/errors/latest

View File

@@ -1,37 +0,0 @@
# Security Policy
This policy outlines the commitment and practices of the go-openapi maintainers regarding security.
## Supported Versions
| Version | Supported |
| ------- | ------------------ |
| 0.x | :white_check_mark: |
## Vulnerability checks in place
This repository uses automated vulnerability scans, at every merged commit and at least once a week.
We use:
* [`GitHub CodeQL`][codeql-url]
* [`trivy`][trivy-url]
* [`govulncheck`][govulncheck-url]
Reports are centralized in github security reports and visible only to the maintainers.
## Reporting a vulnerability
If you become aware of a security vulnerability that affects the current repository,
**please report it privately to the maintainers**
rather than opening a publicly visible GitHub issue.
Please follow the instructions provided by github to [Privately report a security vulnerability][github-guidance-url].
> [!NOTE]
> On Github, navigate to the project's "Security" tab then click on "Report a vulnerability".
[codeql-url]: https://github.com/github/codeql
[trivy-url]: https://trivy.dev/docs/latest/getting-started
[govulncheck-url]: https://go.dev/blog/govulncheck
[github-guidance-url]: https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability#privately-reporting-a-security-vulnerability

View File

@@ -1,11 +1,21 @@
// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers
// SPDX-License-Identifier: Apache-2.0
// Copyright 2015 go-swagger maintainers
//
// 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 errors
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"reflect"
@@ -13,11 +23,9 @@ import (
)
// DefaultHTTPCode is used when the error Code cannot be used as an HTTP code.
//
//nolint:gochecknoglobals // it should have been a constant in the first place, but now it is mutable so we have to leave it here or introduce a breaking change.
var DefaultHTTPCode = http.StatusUnprocessableEntity
// Error represents a error interface all swagger framework errors implement.
// Error represents a error interface all swagger framework errors implement
type Error interface {
error
Code() int32
@@ -28,26 +36,24 @@ type apiError struct {
message string
}
// Error implements the standard error interface.
func (a *apiError) Error() string {
return a.message
}
// Code returns the HTTP status code associated with this error.
func (a *apiError) Code() int32 {
return a.code
}
// MarshalJSON implements the JSON encoding interface.
// MarshalJSON implements the JSON encoding interface
func (a apiError) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]any{
return json.Marshal(map[string]interface{}{
"code": a.code,
"message": a.message,
})
}
// New creates a new API error with a code and a message.
func New(code int32, message string, args ...any) Error {
// New creates a new API error with a code and a message
func New(code int32, message string, args ...interface{}) Error {
if len(args) > 0 {
return &apiError{
code: code,
@@ -60,39 +66,38 @@ func New(code int32, message string, args ...any) Error {
}
}
// NotFound creates a new not found error.
func NotFound(message string, args ...any) Error {
// NotFound creates a new not found error
func NotFound(message string, args ...interface{}) Error {
if message == "" {
message = "Not found"
}
return New(http.StatusNotFound, message, args...)
return New(http.StatusNotFound, fmt.Sprintf(message, args...))
}
// NotImplemented creates a new not implemented error.
// NotImplemented creates a new not implemented error
func NotImplemented(message string) Error {
return New(http.StatusNotImplemented, "%s", message)
return New(http.StatusNotImplemented, message)
}
// MethodNotAllowedError represents an error for when the path matches but the method doesn't.
// MethodNotAllowedError represents an error for when the path matches but the method doesn't
type MethodNotAllowedError struct {
code int32
Allowed []string
message string
}
// Error implements the standard error interface.
func (m *MethodNotAllowedError) Error() string {
return m.message
}
// Code returns 405 (Method Not Allowed) as the HTTP status code.
// Code the error code
func (m *MethodNotAllowedError) Code() int32 {
return m.code
}
// MarshalJSON implements the JSON encoding interface.
// MarshalJSON implements the JSON encoding interface
func (m MethodNotAllowedError) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]any{
return json.Marshal(map[string]interface{}{
"code": m.code,
"message": m.message,
"allowed": m.Allowed,
@@ -110,33 +115,25 @@ func errorAsJSON(err Error) []byte {
func flattenComposite(errs *CompositeError) *CompositeError {
var res []error
for _, err := range errs.Errors {
if err == nil {
continue
for _, er := range errs.Errors {
switch e := er.(type) {
case *CompositeError:
if e != nil && len(e.Errors) > 0 {
flat := flattenComposite(e)
if len(flat.Errors) > 0 {
res = append(res, flat.Errors...)
}
}
default:
if e != nil {
res = append(res, e)
}
}
e := &CompositeError{}
if !errors.As(err, &e) {
res = append(res, err)
continue
}
if len(e.Errors) == 0 {
res = append(res, e)
continue
}
flat := flattenComposite(e)
res = append(res, flat.Errors...)
}
return CompositeValidationError(res...)
}
// MethodNotAllowed creates a new method not allowed error.
// MethodNotAllowed creates a new method not allowed error
func MethodNotAllowed(requested string, allow []string) Error {
msg := fmt.Sprintf("method %s is not allowed, but [%s] are", requested, strings.Join(allow, ","))
return &MethodNotAllowedError{
@@ -146,59 +143,43 @@ func MethodNotAllowed(requested string, allow []string) Error {
}
}
// ServeError implements the [http] error handler interface.
// ServeError implements the http error handler interface
func ServeError(rw http.ResponseWriter, r *http.Request, err error) {
rw.Header().Set("Content-Type", "application/json")
if err == nil {
rw.WriteHeader(http.StatusInternalServerError)
_, _ = rw.Write(errorAsJSON(New(http.StatusInternalServerError, "Unknown error")))
return
}
errComposite := &CompositeError{}
errMethodNotAllowed := &MethodNotAllowedError{}
var errError Error
switch {
case errors.As(err, &errComposite):
er := flattenComposite(errComposite)
switch e := err.(type) {
case *CompositeError:
er := flattenComposite(e)
// strips composite errors to first element only
if len(er.Errors) > 0 {
ServeError(rw, r, er.Errors[0])
return
} else {
// guard against empty CompositeError (invalid construct)
ServeError(rw, r, nil)
}
// guard against empty CompositeError (invalid construct)
ServeError(rw, r, nil)
case errors.As(err, &errMethodNotAllowed):
rw.Header().Add("Allow", strings.Join(errMethodNotAllowed.Allowed, ","))
rw.WriteHeader(asHTTPCode(int(errMethodNotAllowed.Code())))
case *MethodNotAllowedError:
rw.Header().Add("Allow", strings.Join(e.Allowed, ","))
rw.WriteHeader(asHTTPCode(int(e.Code())))
if r == nil || r.Method != http.MethodHead {
_, _ = rw.Write(errorAsJSON(errMethodNotAllowed))
_, _ = rw.Write(errorAsJSON(e))
}
case errors.As(err, &errError):
value := reflect.ValueOf(errError)
case Error:
value := reflect.ValueOf(e)
if value.Kind() == reflect.Ptr && value.IsNil() {
rw.WriteHeader(http.StatusInternalServerError)
_, _ = rw.Write(errorAsJSON(New(http.StatusInternalServerError, "Unknown error")))
return
}
rw.WriteHeader(asHTTPCode(int(errError.Code())))
rw.WriteHeader(asHTTPCode(int(e.Code())))
if r == nil || r.Method != http.MethodHead {
_, _ = rw.Write(errorAsJSON(errError))
_, _ = rw.Write(errorAsJSON(e))
}
case nil:
rw.WriteHeader(http.StatusInternalServerError)
_, _ = rw.Write(errorAsJSON(New(http.StatusInternalServerError, "Unknown error")))
default:
rw.WriteHeader(http.StatusInternalServerError)
if r == nil || r.Method != http.MethodHead {
_, _ = rw.Write(errorAsJSON(New(http.StatusInternalServerError, "%v", err)))
_, _ = rw.Write(errorAsJSON(New(http.StatusInternalServerError, err.Error())))
}
}
}

View File

@@ -1,11 +1,22 @@
// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers
// SPDX-License-Identifier: Apache-2.0
// Copyright 2015 go-swagger maintainers
//
// 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 errors
import "net/http"
// Unauthenticated returns an unauthenticated error.
// Unauthenticated returns an unauthenticated error
func Unauthenticated(scheme string) Error {
return New(http.StatusUnauthorized, "unauthenticated for %s", scheme)
}

View File

@@ -1,13 +1,26 @@
// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers
// SPDX-License-Identifier: Apache-2.0
// Copyright 2015 go-swagger maintainers
//
// 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 errors provides an Error interface and several concrete types
// implementing this interface to manage API errors and JSON-schema validation
// errors.
//
// A middleware handler [ServeError]() is provided to serve the errors types
// it defines.
//
// It is used throughout the various go-openapi toolkit libraries.
// (https://github.com/go-openapi).
/*
Package errors provides an Error interface and several concrete types
implementing this interface to manage API errors and JSON-schema validation
errors.
A middleware handler ServeError() is provided to serve the errors types
it defines.
It is used throughout the various go-openapi toolkit libraries
(https://github.com/go-openapi).
*/
package errors

View File

@@ -1,5 +1,16 @@
// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers
// SPDX-License-Identifier: Apache-2.0
// Copyright 2015 go-swagger maintainers
//
// 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 errors
@@ -9,30 +20,28 @@ import (
"net/http"
)
// Validation represents a failure of a precondition.
type Validation struct { //nolint: errname // changing the name to abide by the naming rule would bring a breaking change.
// Validation represents a failure of a precondition
type Validation struct { //nolint: errname
code int32
Name string
In string
Value any
Value interface{}
message string
Values []any
Values []interface{}
}
// Error implements the standard error interface.
func (e *Validation) Error() string {
return e.message
}
// Code returns the HTTP status code for this validation error.
// Returns 422 (Unprocessable Entity) by default.
// Code the error code
func (e *Validation) Code() int32 {
return e.code
}
// MarshalJSON implements the JSON encoding interface.
// MarshalJSON implements the JSON encoding interface
func (e Validation) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]any{
return json.Marshal(map[string]interface{}{
"code": e.code,
"message": e.message,
"in": e.In,
@@ -42,7 +51,7 @@ func (e Validation) MarshalJSON() ([]byte, error) {
})
}
// ValidateName sets the name for a validation or updates it for a nested property.
// ValidateName sets the name for a validation or updates it for a nested property
func (e *Validation) ValidateName(name string) *Validation {
if name != "" {
if e.Name == "" {
@@ -61,9 +70,9 @@ const (
responseFormatFail = `unsupported media type requested, only %v are available`
)
// InvalidContentType error for an invalid content type.
// InvalidContentType error for an invalid content type
func InvalidContentType(value string, allowed []string) *Validation {
values := make([]any, 0, len(allowed))
values := make([]interface{}, 0, len(allowed))
for _, v := range allowed {
values = append(values, v)
}
@@ -77,9 +86,9 @@ func InvalidContentType(value string, allowed []string) *Validation {
}
}
// InvalidResponseFormat error for an unacceptable response format request.
// InvalidResponseFormat error for an unacceptable response format request
func InvalidResponseFormat(value string, allowed []string) *Validation {
values := make([]any, 0, len(allowed))
values := make([]interface{}, 0, len(allowed))
for _, v := range allowed {
values = append(values, v)
}

View File

@@ -1,5 +1,16 @@
// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers
// SPDX-License-Identifier: Apache-2.0
// Copyright 2015 go-swagger maintainers
//
// 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 errors
@@ -10,14 +21,13 @@ import (
)
// APIVerificationFailed is an error that contains all the missing info for a mismatched section
// between the api registrations and the api spec.
// between the api registrations and the api spec
type APIVerificationFailed struct { //nolint: errname
Section string `json:"section,omitempty"`
MissingSpecification []string `json:"missingSpecification,omitempty"`
MissingRegistration []string `json:"missingRegistration,omitempty"`
}
// Error implements the standard error interface.
func (v *APIVerificationFailed) Error() string {
buf := bytes.NewBuffer(nil)
@@ -25,7 +35,7 @@ func (v *APIVerificationFailed) Error() string {
hasSpecMissing := len(v.MissingSpecification) > 0
if hasRegMissing {
fmt.Fprintf(buf, "missing [%s] %s registrations", strings.Join(v.MissingRegistration, ", "), v.Section)
buf.WriteString(fmt.Sprintf("missing [%s] %s registrations", strings.Join(v.MissingRegistration, ", "), v.Section))
}
if hasRegMissing && hasSpecMissing {
@@ -33,7 +43,7 @@ func (v *APIVerificationFailed) Error() string {
}
if hasSpecMissing {
fmt.Fprintf(buf, "missing from spec file [%s] %s", strings.Join(v.MissingSpecification, ", "), v.Section)
buf.WriteString(fmt.Sprintf("missing from spec file [%s] %s", strings.Join(v.MissingSpecification, ", "), v.Section))
}
return buf.String()

View File

@@ -1,5 +1,16 @@
// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers
// SPDX-License-Identifier: Apache-2.0
// Copyright 2015 go-swagger maintainers
//
// 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 errors
@@ -9,7 +20,7 @@ import (
"net/http"
)
// ParseError represents a parsing error.
// ParseError represents a parsing error
type ParseError struct {
code int32
Name string
@@ -19,7 +30,37 @@ type ParseError struct {
message string
}
// NewParseError creates a new parse error.
func (e *ParseError) Error() string {
return e.message
}
// Code returns the http status code for this error
func (e *ParseError) Code() int32 {
return e.code
}
// MarshalJSON implements the JSON encoding interface
func (e ParseError) MarshalJSON() ([]byte, error) {
var reason string
if e.Reason != nil {
reason = e.Reason.Error()
}
return json.Marshal(map[string]interface{}{
"code": e.code,
"message": e.message,
"in": e.In,
"name": e.Name,
"value": e.Value,
"reason": reason,
})
}
const (
parseErrorTemplContent = `parsing %s %s from %q failed, because %s`
parseErrorTemplContentNoIn = `parsing %s from %q failed, because %s`
)
// NewParseError creates a new parse error
func NewParseError(name, in, value string, reason error) *ParseError {
var msg string
if in == "" {
@@ -36,34 +77,3 @@ func NewParseError(name, in, value string, reason error) *ParseError {
message: msg,
}
}
// Error implements the standard error interface.
func (e *ParseError) Error() string {
return e.message
}
// Code returns 400 (Bad Request) as the HTTP status code for parsing errors.
func (e *ParseError) Code() int32 {
return e.code
}
// MarshalJSON implements the JSON encoding interface.
func (e ParseError) MarshalJSON() ([]byte, error) {
var reason string
if e.Reason != nil {
reason = e.Reason.Error()
}
return json.Marshal(map[string]any{
"code": e.code,
"message": e.message,
"in": e.In,
"name": e.Name,
"value": e.Value,
"reason": reason,
})
}
const (
parseErrorTemplContent = `parsing %s %s from %q failed, because %s`
parseErrorTemplContentNoIn = `parsing %s from %q failed, because %s`
)

View File

@@ -1,11 +1,21 @@
// SPDX-FileCopyrightText: Copyright 2015-2025 go-swagger maintainers
// SPDX-License-Identifier: Apache-2.0
// Copyright 2015 go-swagger maintainers
//
// 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 errors
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
@@ -63,15 +73,14 @@ const (
const maximumValidHTTPCode = 600
// All code responses can be used to differentiate errors for different handling
// by the consuming program.
// by the consuming program
const (
// CompositeErrorCode remains 422 for backwards-compatibility
// and to separate it from validation errors with cause.
// and to separate it from validation errors with cause
CompositeErrorCode = http.StatusUnprocessableEntity
// InvalidTypeCode is used for any subclass of invalid types.
// InvalidTypeCode is used for any subclass of invalid types
InvalidTypeCode = maximumValidHTTPCode + iota
// RequiredFailCode indicates a required field is missing.
RequiredFailCode
TooLongFailCode
TooShortFailCode
@@ -92,26 +101,22 @@ const (
ReadOnlyFailCode
)
// CompositeError is an error that groups several errors together.
// CompositeError is an error that groups several errors together
type CompositeError struct {
Errors []error
code int32
message string
}
// Code returns the HTTP status code for this composite error.
// Code for this error
func (c *CompositeError) Code() int32 {
return c.code
}
// Error implements the standard error interface.
func (c *CompositeError) Error() string {
if len(c.Errors) > 0 {
msgs := []string{c.message + ":"}
for _, e := range c.Errors {
if e == nil {
continue
}
msgs = append(msgs, e.Error())
}
return strings.Join(msgs, "\n")
@@ -119,21 +124,20 @@ func (c *CompositeError) Error() string {
return c.message
}
// Unwrap implements the [errors.Unwrap] interface.
func (c *CompositeError) Unwrap() []error {
return c.Errors
}
// MarshalJSON implements the JSON encoding interface.
// MarshalJSON implements the JSON encoding interface
func (c CompositeError) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]any{
return json.Marshal(map[string]interface{}{
"code": c.code,
"message": c.message,
"errors": c.Errors,
})
}
// CompositeValidationError an error to wrap a bunch of other errors.
// CompositeValidationError an error to wrap a bunch of other errors
func CompositeValidationError(errors ...error) *CompositeError {
return &CompositeError{
code: CompositeErrorCode,
@@ -142,33 +146,20 @@ func CompositeValidationError(errors ...error) *CompositeError {
}
}
// ValidateName recursively sets the name for all validations or updates them for nested properties.
// ValidateName recursively sets the name for all validations or updates them for nested properties
func (c *CompositeError) ValidateName(name string) *CompositeError {
for i, e := range c.Errors {
if e == nil {
continue
}
ce := &CompositeError{}
if errors.As(e, &ce) {
c.Errors[i] = ce.ValidateName(name)
continue
}
ve := &Validation{}
if errors.As(e, &ve) {
if ve, ok := e.(*Validation); ok {
c.Errors[i] = ve.ValidateName(name)
continue
} else if ce, ok := e.(*CompositeError); ok {
c.Errors[i] = ce.ValidateName(name)
}
}
return c
}
// FailedAllPatternProperties an error for when the property doesn't match a pattern.
// FailedAllPatternProperties an error for when the property doesn't match a pattern
func FailedAllPatternProperties(name, in, key string) *Validation {
msg := fmt.Sprintf(failedAllPatternProps, name, key, in)
if in == "" {
@@ -183,7 +174,7 @@ func FailedAllPatternProperties(name, in, key string) *Validation {
}
}
// PropertyNotAllowed an error for when the property doesn't match a pattern.
// PropertyNotAllowed an error for when the property doesn't match a pattern
func PropertyNotAllowed(name, in, key string) *Validation {
msg := fmt.Sprintf(unallowedProperty, name, key, in)
if in == "" {
@@ -198,7 +189,7 @@ func PropertyNotAllowed(name, in, key string) *Validation {
}
}
// TooFewProperties an error for an object with too few properties.
// TooFewProperties an error for an object with too few properties
func TooFewProperties(name, in string, n int64) *Validation {
msg := fmt.Sprintf(tooFewProperties, name, in, n)
if in == "" {
@@ -213,7 +204,7 @@ func TooFewProperties(name, in string, n int64) *Validation {
}
}
// TooManyProperties an error for an object with too many properties.
// TooManyProperties an error for an object with too many properties
func TooManyProperties(name, in string, n int64) *Validation {
msg := fmt.Sprintf(tooManyProperties, name, in, n)
if in == "" {
@@ -228,7 +219,7 @@ func TooManyProperties(name, in string, n int64) *Validation {
}
}
// AdditionalItemsNotAllowed an error for invalid additional items.
// AdditionalItemsNotAllowed an error for invalid additional items
func AdditionalItemsNotAllowed(name, in string) *Validation {
msg := fmt.Sprintf(noAdditionalItems, name, in)
if in == "" {
@@ -242,7 +233,7 @@ func AdditionalItemsNotAllowed(name, in string) *Validation {
}
}
// InvalidCollectionFormat another flavor of invalid type error.
// InvalidCollectionFormat another flavor of invalid type error
func InvalidCollectionFormat(name, in, format string) *Validation {
return &Validation{
code: InvalidTypeCode,
@@ -253,7 +244,7 @@ func InvalidCollectionFormat(name, in, format string) *Validation {
}
}
// InvalidTypeName an error for when the type is invalid.
// InvalidTypeName an error for when the type is invalid
func InvalidTypeName(typeName string) *Validation {
return &Validation{
code: InvalidTypeCode,
@@ -262,8 +253,8 @@ func InvalidTypeName(typeName string) *Validation {
}
}
// InvalidType creates an error for when the type is invalid.
func InvalidType(name, in, typeName string, value any) *Validation {
// InvalidType creates an error for when the type is invalid
func InvalidType(name, in, typeName string, value interface{}) *Validation {
var message string
if in != "" {
@@ -293,9 +284,10 @@ func InvalidType(name, in, typeName string, value any) *Validation {
Value: value,
message: message,
}
}
// DuplicateItems error for when an array contains duplicates.
// DuplicateItems error for when an array contains duplicates
func DuplicateItems(name, in string) *Validation {
msg := fmt.Sprintf(uniqueFail, name, in)
if in == "" {
@@ -309,8 +301,8 @@ func DuplicateItems(name, in string) *Validation {
}
}
// TooManyItems error for when an array contains too many items.
func TooManyItems(name, in string, maximum int64, value any) *Validation {
// TooManyItems error for when an array contains too many items
func TooManyItems(name, in string, maximum int64, value interface{}) *Validation {
msg := fmt.Sprintf(maximumItemsFail, name, in, maximum)
if in == "" {
msg = fmt.Sprintf(maximumItemsFailNoIn, name, maximum)
@@ -325,8 +317,8 @@ func TooManyItems(name, in string, maximum int64, value any) *Validation {
}
}
// TooFewItems error for when an array contains too few items.
func TooFewItems(name, in string, minimum int64, value any) *Validation {
// TooFewItems error for when an array contains too few items
func TooFewItems(name, in string, minimum int64, value interface{}) *Validation {
msg := fmt.Sprintf(minItemsFail, name, in, minimum)
if in == "" {
msg = fmt.Sprintf(minItemsFailNoIn, name, minimum)
@@ -340,8 +332,8 @@ func TooFewItems(name, in string, minimum int64, value any) *Validation {
}
}
// ExceedsMaximumInt error for when maximum validation fails.
func ExceedsMaximumInt(name, in string, maximum int64, exclusive bool, value any) *Validation {
// ExceedsMaximumInt error for when maximumimum validation fails
func ExceedsMaximumInt(name, in string, maximum int64, exclusive bool, value interface{}) *Validation {
var message string
if in == "" {
m := maximumIncFailNoIn
@@ -365,8 +357,8 @@ func ExceedsMaximumInt(name, in string, maximum int64, exclusive bool, value any
}
}
// ExceedsMaximumUint error for when maximum validation fails.
func ExceedsMaximumUint(name, in string, maximum uint64, exclusive bool, value any) *Validation {
// ExceedsMaximumUint error for when maximumimum validation fails
func ExceedsMaximumUint(name, in string, maximum uint64, exclusive bool, value interface{}) *Validation {
var message string
if in == "" {
m := maximumIncFailNoIn
@@ -390,8 +382,8 @@ func ExceedsMaximumUint(name, in string, maximum uint64, exclusive bool, value a
}
}
// ExceedsMaximum error for when maximum validation fails.
func ExceedsMaximum(name, in string, maximum float64, exclusive bool, value any) *Validation {
// ExceedsMaximum error for when maximumimum validation fails
func ExceedsMaximum(name, in string, maximum float64, exclusive bool, value interface{}) *Validation {
var message string
if in == "" {
m := maximumIncFailNoIn
@@ -415,8 +407,8 @@ func ExceedsMaximum(name, in string, maximum float64, exclusive bool, value any)
}
}
// ExceedsMinimumInt error for when minimum validation fails.
func ExceedsMinimumInt(name, in string, minimum int64, exclusive bool, value any) *Validation {
// ExceedsMinimumInt error for when minimum validation fails
func ExceedsMinimumInt(name, in string, minimum int64, exclusive bool, value interface{}) *Validation {
var message string
if in == "" {
m := minIncFailNoIn
@@ -440,8 +432,8 @@ func ExceedsMinimumInt(name, in string, minimum int64, exclusive bool, value any
}
}
// ExceedsMinimumUint error for when minimum validation fails.
func ExceedsMinimumUint(name, in string, minimum uint64, exclusive bool, value any) *Validation {
// ExceedsMinimumUint error for when minimum validation fails
func ExceedsMinimumUint(name, in string, minimum uint64, exclusive bool, value interface{}) *Validation {
var message string
if in == "" {
m := minIncFailNoIn
@@ -465,8 +457,8 @@ func ExceedsMinimumUint(name, in string, minimum uint64, exclusive bool, value a
}
}
// ExceedsMinimum error for when minimum validation fails.
func ExceedsMinimum(name, in string, minimum float64, exclusive bool, value any) *Validation {
// ExceedsMinimum error for when minimum validation fails
func ExceedsMinimum(name, in string, minimum float64, exclusive bool, value interface{}) *Validation {
var message string
if in == "" {
m := minIncFailNoIn
@@ -490,8 +482,8 @@ func ExceedsMinimum(name, in string, minimum float64, exclusive bool, value any)
}
}
// NotMultipleOf error for when multiple of validation fails.
func NotMultipleOf(name, in string, multiple, value any) *Validation {
// NotMultipleOf error for when multiple of validation fails
func NotMultipleOf(name, in string, multiple, value interface{}) *Validation {
var msg string
if in == "" {
msg = fmt.Sprintf(multipleOfFailNoIn, name, multiple)
@@ -507,8 +499,8 @@ func NotMultipleOf(name, in string, multiple, value any) *Validation {
}
}
// EnumFail error for when an enum validation fails.
func EnumFail(name, in string, value any, values []any) *Validation {
// EnumFail error for when an enum validation fails
func EnumFail(name, in string, value interface{}, values []interface{}) *Validation {
var msg string
if in == "" {
msg = fmt.Sprintf(enumFailNoIn, name, values)
@@ -526,8 +518,8 @@ func EnumFail(name, in string, value any, values []any) *Validation {
}
}
// Required error for when a value is missing.
func Required(name, in string, value any) *Validation {
// Required error for when a value is missing
func Required(name, in string, value interface{}) *Validation {
var msg string
if in == "" {
msg = fmt.Sprintf(requiredFailNoIn, name)
@@ -543,8 +535,8 @@ func Required(name, in string, value any) *Validation {
}
}
// ReadOnly error for when a value is present in request.
func ReadOnly(name, in string, value any) *Validation {
// ReadOnly error for when a value is present in request
func ReadOnly(name, in string, value interface{}) *Validation {
var msg string
if in == "" {
msg = fmt.Sprintf(readOnlyFailNoIn, name)
@@ -560,8 +552,8 @@ func ReadOnly(name, in string, value any) *Validation {
}
}
// TooLong error for when a string is too long.
func TooLong(name, in string, maximum int64, value any) *Validation {
// TooLong error for when a string is too long
func TooLong(name, in string, maximum int64, value interface{}) *Validation {
var msg string
if in == "" {
msg = fmt.Sprintf(tooLongMessageNoIn, name, maximum)
@@ -577,8 +569,8 @@ func TooLong(name, in string, maximum int64, value any) *Validation {
}
}
// TooShort error for when a string is too short.
func TooShort(name, in string, minimum int64, value any) *Validation {
// TooShort error for when a string is too short
func TooShort(name, in string, minimum int64, value interface{}) *Validation {
var msg string
if in == "" {
msg = fmt.Sprintf(tooShortMessageNoIn, name, minimum)
@@ -597,7 +589,7 @@ func TooShort(name, in string, minimum int64, value any) *Validation {
// FailedPattern error for when a string fails a regex pattern match
// the pattern that is returned is the ECMA syntax version of the pattern not the golang version.
func FailedPattern(name, in, pattern string, value any) *Validation {
func FailedPattern(name, in, pattern string, value interface{}) *Validation {
var msg string
if in == "" {
msg = fmt.Sprintf(patternFailNoIn, name, pattern)
@@ -615,8 +607,8 @@ func FailedPattern(name, in, pattern string, value any) *Validation {
}
// MultipleOfMustBePositive error for when a
// multipleOf factor is negative.
func MultipleOfMustBePositive(name, in string, factor any) *Validation {
// multipleOf factor is negative
func MultipleOfMustBePositive(name, in string, factor interface{}) *Validation {
return &Validation{
code: MultipleOfMustBePositiveCode,
Name: name,

View File

@@ -318,8 +318,8 @@ github.com/go-openapi/analysis/internal/flatten/operations
github.com/go-openapi/analysis/internal/flatten/replace
github.com/go-openapi/analysis/internal/flatten/schutils
github.com/go-openapi/analysis/internal/flatten/sortref
# github.com/go-openapi/errors v0.22.7
## explicit; go 1.24.0
# github.com/go-openapi/errors v0.22.1
## explicit; go 1.20
github.com/go-openapi/errors
# github.com/go-openapi/jsonpointer v0.21.0
## explicit; go 1.20

View File

@@ -795,7 +795,7 @@ function helm_helper() {
disable_snapshotter_setup=false
for shim in ${HELM_SHIMS}; do
case "${shim}" in
qemu-tdx|qemu-snp)
qemu-snp)
disable_snapshotter_setup=true
break
;;
@@ -804,7 +804,7 @@ function helm_helper() {
# Safety check: Fail if EXPERIMENTAL_SETUP_SNAPSHOTTER is set when using SNP/TDX shims
if [[ "${disable_snapshotter_setup}" == "true" ]] && [[ -n "${HELM_EXPERIMENTAL_SETUP_SNAPSHOTTER}" ]]; then
die "ERROR: HELM_EXPERIMENTAL_SETUP_SNAPSHOTTER cannot be set when using SNP/TDX shims (qemu-snp, qemu-tdx, qemu-nvidia-gpu-snp, qemu-nvidia-gpu-tdx). snapshotter.setup must always be disabled for these shims."
die "ERROR: HELM_EXPERIMENTAL_SETUP_SNAPSHOTTER cannot be set when using SNP shims (qemu-snp). snapshotter.setup must always be disabled for these shims."
fi
if [[ -n "${HELM_EXPERIMENTAL_SETUP_SNAPSHOTTER}" ]]; then

View File

@@ -272,12 +272,29 @@ kbs_uninstall_cli() {
fi
}
# Ensure ~/.cicd/venv exists and activate it in the current shell.
ensure_cicd_python_venv() {
local venv_path="${HOME}/.cicd/venv"
if [[ ! -f "${venv_path}/bin/activate" ]]; then
# NIM tests need Python 3.10 via pyenv; attestation uses system python3. Both are fine.
if command -v pyenv &>/dev/null; then
export PYENV_ROOT="${HOME}/.pyenv"
[[ -d "${PYENV_ROOT}/bin" ]] && export PATH="${PYENV_ROOT}/bin:${PATH}"
eval "$(pyenv init - bash)"
fi
mkdir -p "${HOME}/.cicd"
python3 -m venv "${venv_path}"
fi
# shellcheck disable=SC1091
source "${venv_path}/bin/activate"
}
# Ensure the sev-snp-measure utility is installed.
#
ensure_sev_snp_measure() {
command -v sev-snp-measure >/dev/null && return
source "${HOME}"/.cicd/venv/bin/activate
ensure_cicd_python_venv
pip install sev-snp-measure
}

View File

@@ -176,7 +176,7 @@ function deploy_kata() {
# Workaround to avoid modifying the workflow yaml files
case "${KATA_HYPERVISOR}" in
qemu-nvidia-gpu-*)
qemu-tdx|qemu-nvidia-gpu-*)
USE_EXPERIMENTAL_SETUP_SNAPSHOTTER=true
SNAPSHOTTER="nydus"
EXPERIMENTAL_FORCE_GUEST_PULL=false
@@ -220,7 +220,7 @@ function deploy_kata() {
# deployed when the machine is configured, as on the BM machines).
if [[ ${ARCH} == "x86_64" ]]; then
case "${KATA_HYPERVISOR}" in
qemu-coco-dev*|qemu-nvidia-gpu-*) EXPERIMENTAL_SETUP_SNAPSHOTTER="${SNAPSHOTTER}" ;;
qemu-tdx|qemu-coco-dev*|qemu-nvidia-gpu-*) EXPERIMENTAL_SETUP_SNAPSHOTTER="${SNAPSHOTTER}" ;;
*) ;;
esac
fi

View File

@@ -70,8 +70,7 @@ NGC_API_KEY_SEALED_SECRET_EMBEDQA_BASE64=$(echo -n "${NGC_API_KEY_SEALED_SECRET_
export NGC_API_KEY_SEALED_SECRET_EMBEDQA_BASE64
setup_langchain_flow() {
# shellcheck disable=SC1091 # Sourcing virtual environment activation script
source "${HOME}"/.cicd/venv/bin/activate
ensure_cicd_python_venv
pip install --upgrade pip
[[ "$(pip show langchain 2>/dev/null | awk '/^Version:/{print $2}')" = "0.2.5" ]] || pip install langchain==0.2.5
@@ -177,13 +176,6 @@ setup_file() {
dpkg -s jq >/dev/null 2>&1 || sudo apt -y install jq
export PYENV_ROOT="${HOME}/.pyenv"
[[ -d ${PYENV_ROOT}/bin ]] && export PATH="${PYENV_ROOT}/bin:${PATH}"
eval "$(pyenv init - bash)"
# shellcheck disable=SC1091 # Virtual environment will be created during test execution
python3 -m venv "${HOME}"/.cicd/venv
setup_langchain_flow
policy_settings_dir="$(create_tmp_policy_settings_dir "${pod_config_dir}")"
@@ -262,8 +254,6 @@ setup_file() {
QUESTION="What is the capital of France?"
ANSWER="The capital of France is Paris."
# shellcheck disable=SC1091 # Sourcing virtual environment activation script
source "${HOME}"/.cicd/venv/bin/activate
# shellcheck disable=SC2031 # Variables are used in heredoc, not subshell
cat <<EOF >"${HOME}"/.cicd/venv/langchain_nim.py
from langchain_nvidia_ai_endpoints import ChatNVIDIA
@@ -295,8 +285,6 @@ EOF
# shellcheck disable=SC2031 # Variables are shared via file between BATS tests
[[ -n "${MODEL_NAME}" ]]
# shellcheck disable=SC1091 # Sourcing virtual environment activation script
source "${HOME}"/.cicd/venv/bin/activate
cat <<EOF >"${HOME}"/.cicd/venv/langchain_nim_kata_rag.py
import os
from langchain.chains import ConversationalRetrievalChain, LLMChain

View File

@@ -69,7 +69,14 @@ spec:
limits:
nvidia.com/pgpu: "1"
cpu: "16"
memory: "128Gi"
memory: "64Gi"
volumeMounts:
- name: nim-trusted-cache
mountPath: /opt/nim/.cache
volumes:
- name: nim-trusted-cache
emptyDir:
sizeLimit: 64Gi
---
apiVersion: v1
kind: Secret

View File

@@ -79,7 +79,14 @@ spec:
limits:
nvidia.com/pgpu: "1"
cpu: "16"
memory: "48Gi"
memory: "32Gi"
volumeMounts:
- name: nim-trusted-cache
mountPath: /opt/nim/.cache
volumes:
- name: nim-trusted-cache
emptyDir:
sizeLimit: 40Gi
---
apiVersion: v1
kind: Secret

View File

@@ -150,7 +150,7 @@ install_genpolicy_drop_ins() {
cp "${examples_dir}/20-oci-1.2.0-drop-in.json" "${settings_d}/"
elif is_k3s_or_rke2; then
cp "${examples_dir}/20-oci-1.2.1-drop-in.json" "${settings_d}/"
elif is_nvidia_gpu_platform || [[ -n "${CONTAINER_ENGINE_VERSION:-}" ]]; then
elif is_nvidia_gpu_platform || [[ "${KATA_HYPERVISOR}" == "qemu-tdx" ]] || [[ -n "${CONTAINER_ENGINE_VERSION:-}" ]]; then
cp "${examples_dir}/20-oci-1.3.0-drop-in.json" "${settings_d}/"
fi

View File

@@ -8,7 +8,7 @@ use crate::runtime::containerd;
use crate::utils;
use crate::utils::toml as toml_utils;
use anyhow::Result;
use log::info;
use log::{info, warn};
use std::fs;
use std::path::Path;
@@ -135,6 +135,225 @@ pub async fn configure_snapshotter(
Ok(())
}
/// Clean up all nydus-related entries from containerd's metadata store across all namespaces.
///
/// ## Why this must run before stopping the nydus service
///
/// `ctr snapshots rm` goes through containerd's metadata layer which calls the nydus gRPC
/// backend to physically remove the snapshot. If the service is stopped first, the backend
/// call fails and the BoltDB record is left behind as a stale entry.
///
/// Stale snapshot records in BoltDB cause subsequent image pulls to fail with:
/// "unable to prepare extraction snapshot: target snapshot sha256:...: already exists"
///
/// The failure path: containerd's metadata `Prepare` finds the target chainID in BoltDB and
/// returns AlreadyExists without calling the backend. The unpacker then calls `Stat`, which
/// finds the BoltDB record, but delegates to the backend which returns NotFound (nydus data
/// wiped). The unpacker treats this as a transient race and retries 3 times; all 3 fail the
/// same way, and the pull is aborted.
///
/// ## What we clean
///
/// The containerd BoltDB schema has these nydus-relevant buckets per namespace:
/// - `snapshots/nydus/*` — 100% nydus-specific; MUST be cleaned (triggers the pull bug)
/// - `containers/*` — records carry `snapshotter=nydus` + `snapshotKey`; after
/// removing the snapshots these become dangling references.
/// In a normal CI run they are already gone, but an aborted run
/// can leave orphaned container records that confuse reconciliation.
/// - `images/*` — snapshotter-agnostic (just manifest digest + labels); leave
/// them so the next pull can skip re-downloading content.
/// - `content/blob/*` — shared across all snapshotters; must NOT be removed.
/// - `leases/*`, `ingests/*` — temporary; expire and are GC'd automatically.
///
/// Note: containerd's garbage collector will NOT remove stale snapshots for us, because the
/// image record (a GC root) still references the content blobs which reference the snapshots
/// via gc.ref labels, keeping the entire chain alive in the GC graph.
///
/// ## Snapshot removal ordering
///
/// Snapshots have parent→child relationships; a parent cannot be removed while children
/// exist. The retry loop removes whatever it can each round (leaves first), then retries
/// until nothing remains or no progress is made.
///
/// ## Return value
///
/// Returns `true` only if ALL snapshots were removed from ALL namespaces. A `false` return
/// means at least one snapshot could not be removed — almost certainly because a workload is
/// still actively using it. Callers MUST NOT wipe the nydus data directory in that case:
/// doing so would corrupt running containers whose rootfs mounts still depend on that data.
fn cleanup_containerd_nydus_snapshots(containerd_snapshotter: &str) -> bool {
info!(
"Cleaning up nydus entries from containerd metadata (snapshotter: '{containerd_snapshotter}')"
);
// Discover all containerd namespaces so every namespace is cleaned, not just k8s.io.
let namespaces = match utils::host_exec(&["ctr", "namespaces", "ls", "-q"]) {
Ok(out) => out
.lines()
.map(str::trim)
.filter(|s| !s.is_empty())
.map(String::from)
.collect::<Vec<_>>(),
Err(e) => {
info!("Could not list containerd namespaces ({e}), defaulting to k8s.io");
vec!["k8s.io".to_string()]
}
};
let mut all_clean = true;
for namespace in &namespaces {
cleanup_nydus_containers(namespace, containerd_snapshotter);
if !cleanup_nydus_snapshots(namespace, containerd_snapshotter) {
all_clean = false;
}
}
all_clean
}
/// Remove all container records in this namespace whose snapshotter is the nydus instance.
///
/// Container records carry `snapshotter` and `snapshotKey` fields. After the nydus snapshots
/// are removed these records become dangling references. They do not cause pull failures but
/// can confuse container reconciliation if a previous CI run was aborted mid-test.
fn cleanup_nydus_containers(namespace: &str, containerd_snapshotter: &str) {
// `ctr containers ls` output: ID IMAGE RUNTIME
// We need to cross-reference with `ctr containers info <id>` to filter by snapshotter,
// but that's expensive. Instead we rely on the fact that in the k8s.io namespace every
// container using nydus will have been created by a pod that references it — we can
// safely remove all containers whose snapshot key resolves to a nydus snapshot (i.e. any
// container whose snapshotter field equals our snapshotter name). Since `ctr` does not
// provide a direct --filter for snapshotter on the containers subcommand, we list all
// container IDs, then inspect each one and remove those using the nydus snapshotter.
let output = match utils::host_exec(&["ctr", "-n", namespace, "containers", "ls", "-q"]) {
Ok(out) => out,
Err(e) => {
info!("Namespace {namespace}: cannot list containers ({e}), skipping container cleanup");
return;
}
};
let ids: Vec<String> = output
.lines()
.map(str::trim)
.filter(|s| !s.is_empty())
.map(String::from)
.collect();
if ids.is_empty() {
return;
}
for id in &ids {
// Inspect to check the snapshotter field; output is JSON.
let info = match utils::host_exec(&["ctr", "-n", namespace, "containers", "info", id]) {
Ok(out) => out,
Err(_) => continue,
};
// Simple string search — avoids pulling in a JSON parser.
// The field appears as `"Snapshotter": "nydus"` in the info output.
let snapshotter_pattern = format!("\"Snapshotter\": \"{containerd_snapshotter}\"");
if !info.contains(&snapshotter_pattern) {
continue;
}
match utils::host_exec(&["ctr", "-n", namespace, "containers", "rm", id]) {
Ok(_) => info!("Namespace {namespace}: removed nydus container '{id}'"),
Err(e) => info!("Namespace {namespace}: could not remove container '{id}': {e}"),
}
}
}
/// Remove all snapshot records for the nydus snapshotter from this namespace, with retries.
///
/// Snapshot chains are linear (each layer is one snapshot parented on the previous), so an
/// image with N layers requires exactly N rounds — one leaf removal per round. There is no
/// fixed round limit: the loop terminates naturally once the list is empty (all removed) or
/// makes zero progress (all remaining snapshots are actively mounted by running containers).
///
/// Returns `true` if all snapshots were removed, `false` if any remain (active workloads).
fn cleanup_nydus_snapshots(namespace: &str, containerd_snapshotter: &str) -> bool {
let mut round: u32 = 0;
loop {
round += 1;
// List all snapshots managed by this snapshotter in this namespace.
let output = match utils::host_exec(&[
"ctr",
"-n",
namespace,
"snapshots",
"--snapshotter",
containerd_snapshotter,
"list",
]) {
Ok(out) => out,
Err(e) => {
info!("Namespace {namespace}: cannot list snapshots ({e}), skipping namespace");
return true; // treat as clean: ctr unavailable, nothing we can do
}
};
// Skip the header line; first whitespace-delimited token is the snapshot key.
let keys: Vec<String> = output
.lines()
.skip(1)
.filter_map(|l| {
let k = l.split_whitespace().next()?;
if k.is_empty() {
None
} else {
Some(k.to_string())
}
})
.collect();
if keys.is_empty() {
info!("Namespace {namespace}: no nydus snapshots remaining in containerd metadata");
return true;
}
info!(
"Namespace {namespace}: round {round}: removing {} snapshot(s)",
keys.len()
);
let mut any_removed = false;
for key in &keys {
match utils::host_exec(&[
"ctr",
"-n",
namespace,
"snapshots",
"--snapshotter",
containerd_snapshotter,
"rm",
key,
]) {
Ok(_) => {
any_removed = true;
}
Err(e) => {
// A snapshot cannot be removed when its children still exist.
// This is expected: we remove leaves first, parents in later rounds.
info!("Namespace {namespace}: could not remove snapshot '{key}': {e}");
}
}
}
if !any_removed {
// No progress this round: all remaining snapshots are actively mounted.
// Proceeding to wipe the data directory would corrupt running containers.
warn!(
"Namespace {namespace}: {} snapshot(s) remain after round {round} and none \
could be removed — active workloads are still using them",
keys.len()
);
return false;
}
}
}
pub async fn install_nydus_snapshotter(config: &Config) -> Result<()> {
info!("Deploying nydus-snapshotter");
@@ -143,20 +362,39 @@ pub async fn install_nydus_snapshotter(config: &Config) -> Result<()> {
_ => "nydus-snapshotter".to_string(),
};
// Clean up existing nydus-snapshotter state to ensure fresh start with new version.
// This is safe across all K8s distributions (k3s, rke2, k0s, microk8s, etc.) because
// we only touch the nydus data directory, not containerd's internals.
// When containerd tries to use non-existent snapshots, it will re-pull/re-unpack.
let nydus_data_dir = format!("/host/var/lib/{nydus_snapshotter}");
info!("Cleaning up existing nydus-snapshotter state at {}", nydus_data_dir);
// The containerd proxy_plugins key for this nydus instance.
let containerd_snapshotter = match config.multi_install_suffix.as_ref() {
Some(suffix) if !suffix.is_empty() => format!("nydus-{suffix}"),
_ => "nydus".to_string(),
};
// Stop the service first if it exists (ignore errors if not running)
// Clean up existing nydus-snapshotter state to ensure fresh start with new version.
//
// IMPORTANT: containerd metadata cleanup MUST happen before stopping the nydus service.
// `ctr snapshots rm` goes through containerd's metadata layer which calls the nydus
// gRPC backend to physically remove the snapshot. If the service is stopped first, the
// backend call fails, leaving stale BoltDB records that cause subsequent image pulls to
// fail with "target snapshot sha256:...: already exists" (see cleanup_containerd_nydus_snapshots).
//
// If cleanup returns false, active workloads are still using nydus snapshots. Wiping
// the data directory in that state would corrupt running containers, so we skip it and
// let the new nydus instance start on top of the existing backend state.
let all_clean = cleanup_containerd_nydus_snapshots(&containerd_snapshotter);
// Stop the service now that the metadata has been cleaned.
let _ = utils::host_systemctl(&["stop", &format!("{nydus_snapshotter}.service")]);
// Remove the data directory to clean up old snapshots with potentially incorrect labels
if Path::new(&nydus_data_dir).exists() {
// Only wipe the data directory when the metadata cleanup was complete. If snapshots
// remain (active workloads), preserve the backend so those containers are not broken.
let nydus_data_dir = format!("/host/var/lib/{nydus_snapshotter}");
if all_clean && Path::new(&nydus_data_dir).exists() {
info!("Removing nydus data directory: {}", nydus_data_dir);
fs::remove_dir_all(&nydus_data_dir).ok();
} else if !all_clean {
info!(
"Skipping removal of nydus data directory (active workloads present): {}",
nydus_data_dir
);
}
let config_guest_pulling = "/opt/kata-artifacts/nydus-snapshotter/config-guest-pulling.toml";
@@ -267,6 +505,17 @@ pub async fn uninstall_nydus_snapshotter(config: &Config) -> Result<()> {
_ => "nydus-snapshotter".to_string(),
};
let containerd_snapshotter = match config.multi_install_suffix.as_ref() {
Some(suffix) if !suffix.is_empty() => format!("nydus-{suffix}"),
_ => "nydus".to_string(),
};
// Clean up containerd metadata BEFORE disabling (and thus stopping) the service.
// See install_nydus_snapshotter for the full explanation of why ordering matters.
// If active workloads prevent a full cleanup, skip the data directory removal so
// running containers are not broken.
let all_clean = cleanup_containerd_nydus_snapshots(&containerd_snapshotter);
utils::host_systemctl(&["disable", "--now", &format!("{nydus_snapshotter}.service")])?;
fs::remove_file(format!(
@@ -275,6 +524,16 @@ pub async fn uninstall_nydus_snapshotter(config: &Config) -> Result<()> {
.ok();
fs::remove_dir_all(format!("{}/nydus-snapshotter", config.host_install_dir)).ok();
let nydus_data_dir = format!("/host/var/lib/{nydus_snapshotter}");
if all_clean && Path::new(&nydus_data_dir).exists() {
fs::remove_dir_all(&nydus_data_dir).ok();
} else if !all_clean {
info!(
"Skipping removal of nydus data directory (active workloads present): {}",
nydus_data_dir
);
}
utils::host_systemctl(&["daemon-reload"])?;
Ok(())