mirror of
https://github.com/apple/container.git
synced 2026-09-14 03:35:42 +00:00
Add custom decoder inits for various oci types that use omitempty (#347)
Many fields on the various OCI types use "omitempty" for encoding and decoding the json representation in golang. This PR adds custom json decoder functions to allow for behavior similar to "omitempty". --------- Signed-off-by: Kathryn Baldauf <k_baldauf@apple.com>
This commit is contained in:
@@ -61,6 +61,27 @@ public struct Spec: Codable, Sendable {
|
||||
case root
|
||||
case linux
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
self.init()
|
||||
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
self.version = try container.decode(String.self, forKey: .version)
|
||||
self.hooks = try container.decodeIfPresent(Hook.self, forKey: .hooks)
|
||||
self.process = try container.decodeIfPresent(Process.self, forKey: .process)
|
||||
if let hostname = try container.decodeIfPresent(String.self, forKey: .hostname) {
|
||||
self.hostname = hostname
|
||||
}
|
||||
if let domainname = try container.decodeIfPresent(String.self, forKey: .domainname) {
|
||||
self.domainname = domainname
|
||||
}
|
||||
if let mounts = try container.decodeIfPresent([Mount].self, forKey: .mounts) {
|
||||
self.mounts = mounts
|
||||
}
|
||||
self.annotations = try container.decodeIfPresent([String: String].self, forKey: .annotations)
|
||||
self.root = try container.decodeIfPresent(Root.self, forKey: .root)
|
||||
self.linux = try container.decodeIfPresent(Linux.self, forKey: .linux)
|
||||
}
|
||||
}
|
||||
|
||||
public struct Process: Codable, Sendable {
|
||||
@@ -78,6 +99,22 @@ public struct Process: Codable, Sendable {
|
||||
public var args: [String]
|
||||
public var terminal: Bool
|
||||
|
||||
public enum CodingKeys: String, CodingKey {
|
||||
case cwd
|
||||
case env
|
||||
case consoleSize
|
||||
case selinuxLabel
|
||||
case noNewPrivileges
|
||||
case commandLine
|
||||
case oomScoreAdj
|
||||
case capabilities
|
||||
case apparmorProfile
|
||||
case user
|
||||
case rlimits
|
||||
case args
|
||||
case terminal
|
||||
}
|
||||
|
||||
public init(
|
||||
args: [String] = [],
|
||||
cwd: String = "/",
|
||||
@@ -120,6 +157,40 @@ public struct Process: Codable, Sendable {
|
||||
}()
|
||||
self.init(args: args, cwd: cwd, env: env, user: user)
|
||||
}
|
||||
public init(from decoder: Decoder) throws {
|
||||
self.init()
|
||||
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
self.cwd = try container.decode(String.self, forKey: .cwd)
|
||||
if let env = try container.decodeIfPresent([String].self, forKey: .env) {
|
||||
self.env = env
|
||||
}
|
||||
self.consoleSize = try container.decodeIfPresent(Box.self, forKey: .consoleSize)
|
||||
if let selinuxLabel = try container.decodeIfPresent(String.self, forKey: .selinuxLabel) {
|
||||
self.selinuxLabel = selinuxLabel
|
||||
}
|
||||
if let noNewPrivileges = try container.decodeIfPresent(Bool.self, forKey: .noNewPrivileges) {
|
||||
self.noNewPrivileges = noNewPrivileges
|
||||
}
|
||||
if let commandLine = try container.decodeIfPresent(String.self, forKey: .commandLine) {
|
||||
self.commandLine = commandLine
|
||||
}
|
||||
self.oomScoreAdj = try container.decodeIfPresent(Int.self, forKey: .oomScoreAdj)
|
||||
self.capabilities = try container.decodeIfPresent(LinuxCapabilities.self, forKey: .capabilities)
|
||||
if let apparmorProfile = try container.decodeIfPresent(String.self, forKey: .apparmorProfile) {
|
||||
self.apparmorProfile = apparmorProfile
|
||||
}
|
||||
self.user = try container.decode(User.self, forKey: .user)
|
||||
if let rlimits = try container.decodeIfPresent([POSIXRlimit].self, forKey: .rlimits) {
|
||||
self.rlimits = rlimits
|
||||
}
|
||||
if let args = try container.decodeIfPresent([String].self, forKey: .args) {
|
||||
self.args = args
|
||||
}
|
||||
if let terminal = try container.decodeIfPresent(Bool.self, forKey: .terminal) {
|
||||
self.terminal = terminal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct LinuxCapabilities: Codable, Sendable {
|
||||
@@ -160,6 +231,14 @@ public struct User: Codable, Sendable {
|
||||
public var additionalGids: [UInt32]
|
||||
public var username: String
|
||||
|
||||
public enum CodingKeys: String, CodingKey {
|
||||
case uid
|
||||
case gid
|
||||
case umask
|
||||
case additionalGids
|
||||
case username
|
||||
}
|
||||
|
||||
public init(
|
||||
uid: UInt32 = 0,
|
||||
gid: UInt32 = 0,
|
||||
@@ -173,16 +252,42 @@ public struct User: Codable, Sendable {
|
||||
self.additionalGids = additionalGids
|
||||
self.username = username
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
self.init()
|
||||
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
self.uid = try container.decode(UInt32.self, forKey: .uid)
|
||||
self.gid = try container.decode(UInt32.self, forKey: .gid)
|
||||
self.umask = try container.decodeIfPresent(UInt32.self, forKey: .umask)
|
||||
if let additionalGids = try container.decodeIfPresent([UInt32].self, forKey: .additionalGids) {
|
||||
self.additionalGids = additionalGids
|
||||
}
|
||||
if let username = try container.decodeIfPresent(String.self, forKey: .username) {
|
||||
self.username = username
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct Root: Codable, Sendable {
|
||||
public var path: String
|
||||
public var readonly: Bool
|
||||
|
||||
public enum CodingKeys: String, CodingKey {
|
||||
case path
|
||||
case readonly
|
||||
}
|
||||
|
||||
public init(path: String, readonly: Bool) {
|
||||
self.path = path
|
||||
self.readonly = readonly
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
self.path = try container.decode(String.self, forKey: .path)
|
||||
self.readonly = try container.decodeIfPresent(Bool.self, forKey: .readonly) ?? false
|
||||
}
|
||||
}
|
||||
|
||||
public struct Mount: Codable, Sendable {
|
||||
@@ -194,9 +299,18 @@ public struct Mount: Codable, Sendable {
|
||||
public var uidMappings: [LinuxIDMapping]
|
||||
public var gidMappings: [LinuxIDMapping]
|
||||
|
||||
public enum CodingKeys: String, CodingKey {
|
||||
case type
|
||||
case source
|
||||
case destination
|
||||
case options
|
||||
case uidMappings
|
||||
case gidMappings
|
||||
}
|
||||
|
||||
public init(
|
||||
type: String,
|
||||
source: String,
|
||||
type: String = "",
|
||||
source: String = "",
|
||||
destination: String,
|
||||
options: [String] = [],
|
||||
uidMappings: [LinuxIDMapping] = [],
|
||||
@@ -209,6 +323,16 @@ public struct Mount: Codable, Sendable {
|
||||
self.uidMappings = uidMappings
|
||||
self.gidMappings = gidMappings
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
self.type = try container.decodeIfPresent(String.self, forKey: .type) ?? ""
|
||||
self.source = try container.decodeIfPresent(String.self, forKey: .source) ?? ""
|
||||
self.destination = try container.decode(String.self, forKey: .destination)
|
||||
self.options = try container.decodeIfPresent([String].self, forKey: .options) ?? []
|
||||
self.uidMappings = try container.decodeIfPresent([LinuxIDMapping].self, forKey: .uidMappings) ?? []
|
||||
self.gidMappings = try container.decodeIfPresent([LinuxIDMapping].self, forKey: .gidMappings) ?? []
|
||||
}
|
||||
}
|
||||
|
||||
public struct Hook: Codable, Sendable {
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2025 Apple Inc. and the Containerization project authors.
|
||||
//
|
||||
// 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
|
||||
//
|
||||
// https://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.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import Foundation
|
||||
import Testing
|
||||
|
||||
@testable import ContainerizationOCI
|
||||
|
||||
struct OCISpecTests {
|
||||
@Test func minimalSpecDecode() throws {
|
||||
let version = "1.2.3"
|
||||
let minSpec =
|
||||
"""
|
||||
{
|
||||
"ociVersion": "\(version)"
|
||||
}
|
||||
"""
|
||||
|
||||
guard let data = minSpec.data(using: .utf8) else {
|
||||
Issue.record("test spec is not valid: \(minSpec)")
|
||||
return
|
||||
}
|
||||
|
||||
let decodedSpec = try JSONDecoder().decode(ContainerizationOCI.Spec.self, from: data)
|
||||
#expect(decodedSpec.version == version)
|
||||
#expect(decodedSpec.hooks == nil)
|
||||
#expect(decodedSpec.process == nil)
|
||||
#expect(decodedSpec.hostname == "")
|
||||
#expect(decodedSpec.domainname == "")
|
||||
#expect(decodedSpec.mounts.isEmpty)
|
||||
#expect(decodedSpec.annotations == nil)
|
||||
#expect(decodedSpec.root == nil)
|
||||
#expect(decodedSpec.linux == nil)
|
||||
}
|
||||
|
||||
@Test func minimalProcessSpecDecode() throws {
|
||||
let cwd = "/test"
|
||||
let minProcessSpec =
|
||||
"""
|
||||
{
|
||||
"cwd": "\(cwd)",
|
||||
"user": {
|
||||
"uid": 10,
|
||||
"gid": 11
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
guard let data = minProcessSpec.data(using: .utf8) else {
|
||||
Issue.record("test process spec is not valid: \(minProcessSpec)")
|
||||
return
|
||||
}
|
||||
|
||||
let decodedSpec = try JSONDecoder().decode(ContainerizationOCI.Process.self, from: data)
|
||||
#expect(decodedSpec.cwd == cwd)
|
||||
#expect(decodedSpec.env.isEmpty)
|
||||
#expect(decodedSpec.consoleSize == nil)
|
||||
#expect(decodedSpec.selinuxLabel == "")
|
||||
#expect(decodedSpec.noNewPrivileges == false)
|
||||
#expect(decodedSpec.commandLine == "")
|
||||
#expect(decodedSpec.oomScoreAdj == nil)
|
||||
#expect(decodedSpec.capabilities == nil)
|
||||
#expect(decodedSpec.apparmorProfile == "")
|
||||
#expect(decodedSpec.user.uid == 10)
|
||||
#expect(decodedSpec.user.gid == 11)
|
||||
#expect(decodedSpec.rlimits.isEmpty)
|
||||
#expect(decodedSpec.terminal == false)
|
||||
}
|
||||
|
||||
@Test func minimalUserSpecDecode() throws {
|
||||
let minUserSpec =
|
||||
"""
|
||||
{
|
||||
"uid": 10,
|
||||
"gid": 11
|
||||
}
|
||||
"""
|
||||
|
||||
guard let data = minUserSpec.data(using: .utf8) else {
|
||||
Issue.record("test user spec is not valid: \(minUserSpec)")
|
||||
return
|
||||
}
|
||||
|
||||
let decodedSpec = try JSONDecoder().decode(ContainerizationOCI.User.self, from: data)
|
||||
#expect(decodedSpec.uid == 10)
|
||||
#expect(decodedSpec.gid == 11)
|
||||
#expect(decodedSpec.umask == nil)
|
||||
#expect(decodedSpec.additionalGids.isEmpty)
|
||||
#expect(decodedSpec.username == "")
|
||||
}
|
||||
|
||||
@Test func minimalRootSpecDecode() throws {
|
||||
let path = "/testpath"
|
||||
let minRootSpec =
|
||||
"""
|
||||
{
|
||||
"path": "\(path)"
|
||||
}
|
||||
"""
|
||||
|
||||
guard let data = minRootSpec.data(using: .utf8) else {
|
||||
Issue.record("test root spec is not valid: \(minRootSpec)")
|
||||
return
|
||||
}
|
||||
|
||||
let decodedSpec = try JSONDecoder().decode(ContainerizationOCI.Root.self, from: data)
|
||||
#expect(decodedSpec.path == path)
|
||||
#expect(decodedSpec.readonly == false)
|
||||
}
|
||||
|
||||
@Test func minimalMountSpecDecode() throws {
|
||||
let destination = "/testdest"
|
||||
let minMountSpec =
|
||||
"""
|
||||
{
|
||||
"destination": "\(destination)"
|
||||
}
|
||||
"""
|
||||
|
||||
guard let data = minMountSpec.data(using: .utf8) else {
|
||||
Issue.record("test mount spec is not valid: \(minMountSpec)")
|
||||
return
|
||||
}
|
||||
|
||||
let decodedSpec = try JSONDecoder().decode(ContainerizationOCI.Mount.self, from: data)
|
||||
#expect(decodedSpec.type == "")
|
||||
#expect(decodedSpec.source == "")
|
||||
#expect(decodedSpec.destination == destination)
|
||||
#expect(decodedSpec.options.isEmpty)
|
||||
#expect(decodedSpec.uidMappings.isEmpty)
|
||||
#expect(decodedSpec.gidMappings.isEmpty)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user