Skip to content

Commit 6d82c4e

Browse files
committed
feat(wasm): add support for oci urls
1 parent c69308b commit 6d82c4e

6 files changed

Lines changed: 428 additions & 8 deletions

File tree

‎docs/reference/config.md‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ sql:
1414
queries: "postgresql/query.sql"
1515
engine: "postgresql"
1616
gen:
17-
go:
17+
go:
1818
package: "authors"
1919
out: "postgresql"
2020
database:
@@ -122,7 +122,7 @@ The `analyzer` mapping supports the following keys:
122122

123123
- `database`:
124124
- If false, do not use the configured database for query analysis. Defaults to `true`.
125-
125+
126126
### gen
127127

128128
The `gen` mapping supports the following keys:
@@ -254,21 +254,21 @@ Each mapping in the `plugins` collection has the following keys:
254254
- The format expected. Supports `json` and `protobuf` formats. Defaults to `protobuf`.
255255
- `wasm`: A mapping with a two keys `url` and `sha256`
256256
- `url`:
257-
- The URL to fetch the WASM file. Supports the `https://` or `file://` schemes.
257+
- The URL to fetch the WASM file. Supports the `https://`, `file://`, or `oci://` schemes. OCI artifacts may be referenced by tag or digest and must use the [CNCF Wasm OCI artifact layout](https://tag-runtime.cncf.io/wgs/wasm/deliverables/wasm-oci-artifact/): exactly one `application/wasm` layer and a config with media type `application/vnd.wasm.config.v0+json`, `architecture: "wasm"`, `os: "wasip1"`, and `layerDigests` matching the layer digest. WASI Preview 2 components are not supported. Registry authentication uses the Docker config, including configured credential helpers.
258258
- `sha256`
259259
- The SHA256 checksum for the downloaded file.
260-
260+
261261
```yaml
262262
version: "2"
263263
plugins:
264264
- name: "py"
265-
wasm:
265+
wasm:
266266
url: "https://github.com/sqlc-dev/sqlc-gen-python/releases/download/v0.16.0-alpha/sqlc-gen-python.wasm"
267267
sha256: "428476c7408fd4c032da4ec74e8a7344f4fa75e0f98a5a3302f238283b9b95f2"
268268
- name: "js"
269269
env:
270270
- PATH
271-
process:
271+
process:
272272
cmd: "sqlc-gen-json"
273273
```
274274

@@ -285,7 +285,7 @@ Each mapping in the `rules` collection has the following keys:
285285

286286
See the [vet](../howto/vet.md) documentation for a list of built-in rules and
287287
help writing custom rules.
288-
288+
289289
```yaml
290290
version: "2"
291291
sql:
@@ -319,7 +319,7 @@ rules:
319319
rule: |
320320
query.cmd == "exec"
321321
```
322-
322+
323323
### Global overrides
324324

325325
Sometimes, the same configuration must be done across various specifications of

‎go.mod‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ require (
1414
github.com/jackc/pgx/v5 v5.11.0
1515
github.com/jinzhu/inflection v1.0.0
1616
github.com/ncruces/go-sqlite3 v0.35.6
17+
github.com/opencontainers/go-digest v1.0.0
18+
github.com/opencontainers/image-spec v1.1.1
1719
github.com/riza-io/grpc-go v0.2.0
1820
github.com/spf13/cobra v1.10.2
1921
github.com/spf13/pflag v1.0.10
@@ -31,6 +33,7 @@ require (
3133
google.golang.org/grpc v1.84.0
3234
google.golang.org/protobuf v1.36.12
3335
gopkg.in/yaml.v3 v3.0.1
36+
oras.land/oras-go/v2 v2.6.2
3437
)
3538

3639
require (

‎go.sum‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ github.com/ncruces/go-sqlite3-wasm/v6 v6.3.35304 h1:dBSZlcEFdtBMvNRg34y50mConBPO
4343
github.com/ncruces/go-sqlite3-wasm/v6 v6.3.35304/go.mod h1:YvoJzbJpX6phd3BGdtiXu2NuD5RX6G8zsUdzt47GgOY=
4444
github.com/ncruces/julianday v1.0.0 h1:fH0OKwa7NWvniGQtxdJRxAgkBMolni2BjDHaWTxqt7M=
4545
github.com/ncruces/julianday v1.0.0/go.mod h1:Dusn2KvZrrovOMJuOt0TNXL6tB7U2E8kvza5fFc9G7g=
46+
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
47+
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
48+
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
49+
github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
4650
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
4751
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
4852
github.com/riza-io/grpc-go v0.2.0 h1:2HxQKFVE7VuYstcJ8zqpN84VnAoJ4dCL6YFhJewNcHQ=
@@ -114,3 +118,5 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV
114118
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
115119
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
116120
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
121+
oras.land/oras-go/v2 v2.6.2 h1:N04RXngAp1LJKTG6ifz3xHPipasEkWr+hFmInja5YKo=
122+
oras.land/oras-go/v2 v2.6.2/go.mod h1:PlTtg4JTDJkDe8yVHpM2wz7/YDc00GVas+i4jAW2TZ4=

‎internal/ext/wasm/oci.go‎

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
package wasm
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"runtime"
8+
"strings"
9+
10+
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
11+
"oras.land/oras-go/v2/content"
12+
"oras.land/oras-go/v2/registry/remote"
13+
"oras.land/oras-go/v2/registry/remote/auth"
14+
"oras.land/oras-go/v2/registry/remote/credentials"
15+
"oras.land/oras-go/v2/registry/remote/retry"
16+
17+
"github.com/sqlc-dev/sqlc/internal/info"
18+
)
19+
20+
const (
21+
wasmLayerMediaType = "application/wasm"
22+
wasmConfigMediaType = "application/vnd.wasm.config.v0+json"
23+
)
24+
25+
type wasmConfig struct {
26+
Architecture string `json:"architecture"`
27+
OS string `json:"os"`
28+
LayerDigests []string `json:"layerDigests"`
29+
}
30+
31+
func fetchOCI(ctx context.Context, uri string, plainHTTP bool) ([]byte, error) {
32+
reference := strings.TrimPrefix(uri, "oci://")
33+
repo, err := remote.NewRepository(reference)
34+
if err != nil {
35+
return nil, fmt.Errorf("parse OCI reference %q: %w", uri, err)
36+
}
37+
repo.PlainHTTP = plainHTTP
38+
39+
store, err := credentials.NewStoreFromDocker(credentials.StoreOptions{})
40+
if err != nil {
41+
return nil, fmt.Errorf("load Docker credentials: %w", err)
42+
}
43+
client := &auth.Client{
44+
Client: retry.DefaultClient,
45+
Cache: auth.NewCache(),
46+
Credential: credentials.Credential(store),
47+
}
48+
client.SetUserAgent(fmt.Sprintf("sqlc/%s Go/%s (%s %s)", info.Version, runtime.Version(), runtime.GOOS, runtime.GOARCH))
49+
repo.Client = client
50+
51+
desc, manifestReader, err := repo.FetchReference(ctx, repo.Reference.Reference)
52+
if err != nil {
53+
return nil, fmt.Errorf("fetch OCI manifest %q: %w", uri, err)
54+
}
55+
defer manifestReader.Close()
56+
if desc.MediaType != ocispec.MediaTypeImageManifest {
57+
return nil, fmt.Errorf("OCI reference %q resolved to unsupported media type %q", uri, desc.MediaType)
58+
}
59+
manifestData, err := content.ReadAll(manifestReader, desc)
60+
if err != nil {
61+
return nil, fmt.Errorf("read OCI manifest %q: %w", uri, err)
62+
}
63+
var manifest ocispec.Manifest
64+
if err := json.Unmarshal(manifestData, &manifest); err != nil {
65+
return nil, fmt.Errorf("decode OCI manifest %q: %w", uri, err)
66+
}
67+
if manifest.SchemaVersion != 2 {
68+
return nil, fmt.Errorf("OCI artifact %q: unsupported schema version %d", uri, manifest.SchemaVersion)
69+
}
70+
layer, err := wasmLayer(manifest.Layers)
71+
if err != nil {
72+
return nil, fmt.Errorf("OCI artifact %q: %w", uri, err)
73+
}
74+
if manifest.Config.MediaType != wasmConfigMediaType {
75+
return nil, fmt.Errorf("OCI artifact %q: unsupported config media type %q", uri, manifest.Config.MediaType)
76+
}
77+
configData, err := content.FetchAll(ctx, repo, manifest.Config)
78+
if err != nil {
79+
return nil, fmt.Errorf("fetch OCI WASM config %q: %w", uri, err)
80+
}
81+
if err := validateWASMConfig(configData, layer); err != nil {
82+
return nil, fmt.Errorf("OCI artifact %q: %w", uri, err)
83+
}
84+
85+
body, err := repo.Fetch(ctx, layer)
86+
if err != nil {
87+
return nil, fmt.Errorf("fetch OCI WASM layer %q: %w", uri, err)
88+
}
89+
defer body.Close()
90+
wmod, err := content.ReadAll(body, layer)
91+
if err != nil {
92+
return nil, fmt.Errorf("read OCI WASM layer %q: %w", uri, err)
93+
}
94+
return wmod, nil
95+
}
96+
97+
func wasmLayer(layers []ocispec.Descriptor) (ocispec.Descriptor, error) {
98+
if len(layers) != 1 {
99+
return ocispec.Descriptor{}, fmt.Errorf("manifest must contain exactly one WASM layer")
100+
}
101+
if layers[0].MediaType != wasmLayerMediaType {
102+
return ocispec.Descriptor{}, fmt.Errorf("unsupported WASM layer media type %q", layers[0].MediaType)
103+
}
104+
return layers[0], nil
105+
}
106+
107+
func validateWASMConfig(data []byte, layer ocispec.Descriptor) error {
108+
var config wasmConfig
109+
if err := json.Unmarshal(data, &config); err != nil {
110+
return fmt.Errorf("decode WASM config: %w", err)
111+
}
112+
if config.Architecture != "wasm" {
113+
return fmt.Errorf("unsupported WASM architecture %q", config.Architecture)
114+
}
115+
if config.OS != "wasip1" {
116+
return fmt.Errorf("unsupported WASM OS %q: only wasip1 plugins are supported", config.OS)
117+
}
118+
if len(config.LayerDigests) != 1 || config.LayerDigests[0] != layer.Digest.String() {
119+
return fmt.Errorf("WASM config layerDigests must match the manifest layers")
120+
}
121+
return nil
122+
}

0 commit comments

Comments
 (0)