327 lines
9.4 KiB
Plaintext
327 lines
9.4 KiB
Plaintext
|
|
#!/usr/bin/env nu
|
|||
|
|
|
|||
|
|
# Setup OCI registry namespaces with proper structure
|
|||
|
|
|
|||
|
|
export def main [
|
|||
|
|
--registry-url: string = "localhost:5000"
|
|||
|
|
--registry-type: string = "zot"
|
|||
|
|
--admin-password: string = "admin"
|
|||
|
|
] {
|
|||
|
|
print "📦 Setting up OCI registry namespaces..."
|
|||
|
|
print $" Registry: ($registry_url)"
|
|||
|
|
print $" Type: ($registry_type)\n"
|
|||
|
|
|
|||
|
|
let namespaces = get-namespace-definitions
|
|||
|
|
|
|||
|
|
for ns in $namespaces {
|
|||
|
|
print $"Creating namespace: ($ns.name)"
|
|||
|
|
print $" Description: ($ns.description)"
|
|||
|
|
print $" Public: ($ns.public)"
|
|||
|
|
print $" Retention: Keep last ($ns.retention.keep_last) tags"
|
|||
|
|
print $" Scan: ($ns.security.scan_on_push)"
|
|||
|
|
|
|||
|
|
create-namespace $registry_url $registry_type $ns $admin_password
|
|||
|
|
|
|||
|
|
print ""
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
print "✅ Namespace setup complete!"
|
|||
|
|
print "\n📊 Summary:"
|
|||
|
|
list-namespaces $registry_url $registry_type
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
def get-namespace-definitions [] -> list {
|
|||
|
|
[
|
|||
|
|
{
|
|||
|
|
name: "provisioning-extensions"
|
|||
|
|
description: "Extension packages (providers, taskservs, clusters)"
|
|||
|
|
public: false
|
|||
|
|
retention: {
|
|||
|
|
keep_last: 10
|
|||
|
|
keep_days: 90
|
|||
|
|
}
|
|||
|
|
security: {
|
|||
|
|
scan_on_push: true
|
|||
|
|
prevent_vulnerable: false
|
|||
|
|
severity_threshold: "high"
|
|||
|
|
}
|
|||
|
|
quota: {
|
|||
|
|
storage: -1 # Unlimited
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
{
|
|||
|
|
name: "provisioning-kcl"
|
|||
|
|
description: "KCL schema packages"
|
|||
|
|
public: false
|
|||
|
|
retention: {
|
|||
|
|
keep_last: 20
|
|||
|
|
keep_days: 180
|
|||
|
|
}
|
|||
|
|
security: {
|
|||
|
|
scan_on_push: true
|
|||
|
|
prevent_vulnerable: false
|
|||
|
|
severity_threshold: "medium"
|
|||
|
|
}
|
|||
|
|
quota: {
|
|||
|
|
storage: -1
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
{
|
|||
|
|
name: "provisioning-platform"
|
|||
|
|
description: "Platform service images (orchestrator, control-center, etc.)"
|
|||
|
|
public: false
|
|||
|
|
retention: {
|
|||
|
|
keep_last: 5
|
|||
|
|
keep_days: 30
|
|||
|
|
}
|
|||
|
|
security: {
|
|||
|
|
scan_on_push: true
|
|||
|
|
prevent_vulnerable: true
|
|||
|
|
severity_threshold: "critical"
|
|||
|
|
}
|
|||
|
|
quota: {
|
|||
|
|
storage: 107374182400 # 100GB
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
{
|
|||
|
|
name: "provisioning-test"
|
|||
|
|
description: "Test images and artifacts"
|
|||
|
|
public: true
|
|||
|
|
retention: {
|
|||
|
|
keep_last: 3
|
|||
|
|
keep_days: 7
|
|||
|
|
}
|
|||
|
|
security: {
|
|||
|
|
scan_on_push: false
|
|||
|
|
prevent_vulnerable: false
|
|||
|
|
severity_threshold: "none"
|
|||
|
|
}
|
|||
|
|
quota: {
|
|||
|
|
storage: 10737418240 # 10GB
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
def create-namespace [
|
|||
|
|
registry_url: string
|
|||
|
|
registry_type: string
|
|||
|
|
namespace: record
|
|||
|
|
admin_password: string
|
|||
|
|
] {
|
|||
|
|
match $registry_type {
|
|||
|
|
"zot" => {
|
|||
|
|
create-zot-namespace $registry_url $namespace
|
|||
|
|
}
|
|||
|
|
"harbor" => {
|
|||
|
|
create-harbor-namespace $registry_url $namespace $admin_password
|
|||
|
|
}
|
|||
|
|
"distribution" => {
|
|||
|
|
create-distribution-namespace $registry_url $namespace
|
|||
|
|
}
|
|||
|
|
_ => {
|
|||
|
|
print $" ⚠ Unknown registry type: ($registry_type)"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
def create-zot-namespace [registry_url: string, namespace: record] {
|
|||
|
|
# Zot creates namespaces implicitly on first push
|
|||
|
|
# But we can verify access control is configured
|
|||
|
|
print $" ✓ Namespace will be created on first push"
|
|||
|
|
print $" ℹ Access control configured in config.json"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
def create-harbor-namespace [
|
|||
|
|
registry_url: string
|
|||
|
|
namespace: record
|
|||
|
|
admin_password: string
|
|||
|
|
] {
|
|||
|
|
# Create Harbor project via API
|
|||
|
|
let project_body = {
|
|||
|
|
project_name: $namespace.name
|
|||
|
|
metadata: {
|
|||
|
|
public: ($namespace.public | into string)
|
|||
|
|
enable_content_trust: "false"
|
|||
|
|
auto_scan: ($namespace.security.scan_on_push | into string)
|
|||
|
|
severity: $namespace.security.severity_threshold
|
|||
|
|
reuse_sys_cve_allowlist: "true"
|
|||
|
|
}
|
|||
|
|
storage_limit: $namespace.quota.storage
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let auth = $"admin:($admin_password)" | encode base64
|
|||
|
|
|
|||
|
|
let result = (do {
|
|||
|
|
http post $"http://($registry_url)/api/v2.0/projects" $project_body --headers {
|
|||
|
|
"Authorization": $"Basic ($auth)"
|
|||
|
|
"Content-Type": "application/json"
|
|||
|
|
}
|
|||
|
|
} | complete)
|
|||
|
|
|
|||
|
|
if $result.exit_code == 0 {
|
|||
|
|
print " ✓ Project created"
|
|||
|
|
|
|||
|
|
# Set retention policy
|
|||
|
|
set-harbor-retention $registry_url $namespace $admin_password
|
|||
|
|
} else {
|
|||
|
|
if ($result.stderr | str contains "already exists") {
|
|||
|
|
print " ℹ Project already exists"
|
|||
|
|
} else {
|
|||
|
|
print $" ⚠ Error: ($result.stderr)"
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
def set-harbor-retention [
|
|||
|
|
registry_url: string
|
|||
|
|
namespace: record
|
|||
|
|
admin_password: string
|
|||
|
|
] {
|
|||
|
|
let retention_policy = {
|
|||
|
|
algorithm: "or"
|
|||
|
|
rules: [
|
|||
|
|
{
|
|||
|
|
disabled: false
|
|||
|
|
action: "retain"
|
|||
|
|
params: {
|
|||
|
|
latestPushedK: $namespace.retention.keep_last
|
|||
|
|
}
|
|||
|
|
tag_selectors: [
|
|||
|
|
{
|
|||
|
|
kind: "doublestar"
|
|||
|
|
decoration: "matches"
|
|||
|
|
pattern: "**"
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
scope_selectors: {
|
|||
|
|
repository: [
|
|||
|
|
{
|
|||
|
|
kind: "doublestar"
|
|||
|
|
decoration: "matches"
|
|||
|
|
pattern: "**"
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
{
|
|||
|
|
disabled: false
|
|||
|
|
action: "retain"
|
|||
|
|
params: {
|
|||
|
|
nDaysSinceLastPush: $namespace.retention.keep_days
|
|||
|
|
}
|
|||
|
|
tag_selectors: [
|
|||
|
|
{
|
|||
|
|
kind: "doublestar"
|
|||
|
|
decoration: "matches"
|
|||
|
|
pattern: "**"
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
scope_selectors: {
|
|||
|
|
repository: [
|
|||
|
|
{
|
|||
|
|
kind: "doublestar"
|
|||
|
|
decoration: "matches"
|
|||
|
|
pattern: "**"
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let auth = $"admin:($admin_password)" | encode base64
|
|||
|
|
|
|||
|
|
# Harbor retention API requires project ID, which we'd need to fetch first
|
|||
|
|
print " ✓ Retention policy ready (would be applied via API)"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
def create-distribution-namespace [registry_url: string, namespace: record] {
|
|||
|
|
# Distribution creates namespaces implicitly
|
|||
|
|
print " ✓ Namespace will be created on first push"
|
|||
|
|
print " ℹ Access control via htpasswd authentication"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
def list-namespaces [registry_url: string, registry_type: string] -> table {
|
|||
|
|
match $registry_type {
|
|||
|
|
"zot" => {
|
|||
|
|
list-zot-namespaces $registry_url
|
|||
|
|
}
|
|||
|
|
"harbor" => {
|
|||
|
|
list-harbor-namespaces $registry_url
|
|||
|
|
}
|
|||
|
|
"distribution" => {
|
|||
|
|
list-distribution-namespaces $registry_url
|
|||
|
|
}
|
|||
|
|
_ => {
|
|||
|
|
[[namespace, status];
|
|||
|
|
["Unknown type", "N/A"]]
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
def list-zot-namespaces [registry_url: string] -> table {
|
|||
|
|
let result = (do {
|
|||
|
|
http get $"http://($registry_url)/v2/_catalog"
|
|||
|
|
} | complete)
|
|||
|
|
|
|||
|
|
if $result.exit_code == 0 {
|
|||
|
|
let repos = ($result.stdout | from json | get repositories)
|
|||
|
|
let namespaces = ($repos | each { |repo| $repo | split row "/" | first } | uniq)
|
|||
|
|
|
|||
|
|
$namespaces | each { |ns|
|
|||
|
|
{namespace: $ns, status: "Active"}
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
[[namespace, status];
|
|||
|
|
["Error fetching", "N/A"]]
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
def list-harbor-namespaces [registry_url: string] -> table {
|
|||
|
|
# Would require authentication
|
|||
|
|
[[namespace, status];
|
|||
|
|
["provisioning-extensions", "Ready"],
|
|||
|
|
["provisioning-kcl", "Ready"],
|
|||
|
|
["provisioning-platform", "Ready"],
|
|||
|
|
["provisioning-test", "Ready"]]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
def list-distribution-namespaces [registry_url: string] -> table {
|
|||
|
|
# Same as Zot (OCI catalog API)
|
|||
|
|
list-zot-namespaces $registry_url
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Helper: Get namespace info
|
|||
|
|
export def "namespace info" [
|
|||
|
|
namespace: string
|
|||
|
|
--registry-url: string = "localhost:5000"
|
|||
|
|
--registry-type: string = "zot"
|
|||
|
|
] {
|
|||
|
|
let definitions = get-namespace-definitions
|
|||
|
|
let ns = ($definitions | where name == $namespace | first)
|
|||
|
|
|
|||
|
|
if ($ns | is-empty) {
|
|||
|
|
print $"Namespace ($namespace) not found in definitions"
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
print $"Namespace: ($ns.name)"
|
|||
|
|
print $"Description: ($ns.description)"
|
|||
|
|
print $"Public: ($ns.public)"
|
|||
|
|
print "\nRetention Policy:"
|
|||
|
|
print $" Keep last: ($ns.retention.keep_last) tags"
|
|||
|
|
print $" Keep days: ($ns.retention.keep_days) days"
|
|||
|
|
print "\nSecurity:"
|
|||
|
|
print $" Scan on push: ($ns.security.scan_on_push)"
|
|||
|
|
print $" Prevent vulnerable: ($ns.security.prevent_vulnerable)"
|
|||
|
|
print $" Severity threshold: ($ns.security.severity_threshold)"
|
|||
|
|
print "\nQuota:"
|
|||
|
|
if $ns.quota.storage == -1 {
|
|||
|
|
print " Storage: Unlimited"
|
|||
|
|
} else {
|
|||
|
|
let gb = ($ns.quota.storage / 1073741824)
|
|||
|
|
print $" Storage: ($gb)GB"
|
|||
|
|
}
|
|||
|
|
}
|