/*
 * Infix Containers YANG module
 */
module infix-containers {
  yang-version 1.1;
  namespace "urn:ietf:params:xml:ns:yang:infix-containers";
  prefix infix-cont;

  import ietf-interfaces {
    prefix if;
  }

  import ietf-inet-types {
    prefix inet;
  }

  import infix-interfaces {
    prefix infix-if;
  }

  import infix-system {
    prefix infix-sys;
  }

  revision 2026-02-23 {
    description "Input validation improvements";
    reference "internal";
  }

  revision 2025-12-09 {
    description "Add resource management:
                  - Add resource-limit container with memory and cpu configuration.
                  - Add resource-usage operational data for live resource usage statistics.";
    reference "internal";
  }

  revision 2025-10-12 {
    description "Two major changes:
                  - Add dedicated 'ident' type for container and volume names.
                  - New upgrade action (RPC) for containers with :latest image.";
    reference "internal";
  }

  revision 2025-06-25 {
    description "Add file mode option to content mounts, allows creating scripts.";
    reference "internal";
  }

  revision 2025-05-14 {
    description
      "Validation improvement:
       - Added constraint to require either 'source' or 'content' in a container mount.";
    reference "internal";
  }

  revision 2024-11-15 {
    description "Two major changes:
                  - Add support for ftp/http/https images with checksum
                  - Deprecate read-only, it is now always true";
    reference "internal";
  }

  revision 2024-10-14 {
    description "Two major changes:
                  - Allow changing name of host interfaces inside container
                  - Support hostname format specifiers, like ietf-system";
    reference "internal";
  }

  revision 2024-03-27 {
    description "Add support for capabilities.";
    reference "internal";
  }

  revision 2024-02-01 {
    description "Initial revision";
    reference "internal";
  }

  /*
   * Typedefs
   */

  typedef ident {
    description "Container name or volume identifiers may not contain spaces.";
    type string {
      length "2..64";
      pattern '[a-zA-Z0-9][a-zA-Z0-9_.-]+';
    }
  }


  typedef image-reference {
    description "OCI image reference or transport URI.
                 Allows registry references (e.g., quay.io/user/image:tag),
                 transport prefixes (docker://, oci-archive:, dir:), and
                 download URLs (ftp://, http://, https://).";
    type string {
      pattern '[a-zA-Z0-9][a-zA-Z0-9_./:@=+%~-]*';
    }
  }

  typedef mount-type {
    type enumeration {
      enum bind {
        description "Regular bind mount of host path to container.";
        value 1;
      }
      enum glob {
        description "Glob match and bind mount matching host paths to container.";
        value 2;
      }
    }
  }

  typedef restart-policy {
    type enumeration {
      enum never {
	description "Do not restart containers that exit/crash.";
	value 1;
      }
      enum retry {
	description "Restart containers up to 10 times before giving up.";
	value 2;
      }
      enum always {
	description "Always restart containers when they exit.";
	value 3;
      }
    }
  }

  typedef capabilities {
    type enumeration {
      enum dac_override;
      enum fsetid;
      enum net_admin;
      enum net_bind_service;
      enum net_raw;
      enum setgid;
      enum setuid;
      enum setpcap;
      enum syslog;
      enum sys_admin;
      enum sys_chroot;
      enum sys_module;
      enum sys_ptrace;
      enum sys_rawio;
      enum sys_time;
    }
  }

  /*
   * Data Nodes
   */

  container containers {
    list container {
      key "name";

      leaf enabled {
	description "Enable or disable a container configuration.";
	type boolean;
	default true;
      }

      leaf name {
	description "Name of the container";
	type ident;
      }

      leaf id {
        description "Container ID, unique hash.";
        config false;
        type string;
      }

      leaf image {
	description "Docker image for the container: [transport]name[:tag|@digest]

                     quay.io/username/myimage     -- Pull myimage:latest
                     docker://busybox             -- Pull busybox:latest from Docker Hub
                     docker://ghcr.io/usr/img     -- Pull img:latest from GitHub packages
                     dir:/media/usb/myimage:1.1   -- Use myimage v1.1 from USB media
                     docker-archive:/tmp/archive  -- Use archive:latest from tarball
                     oci-archive:/lib/oci/archive -- Use archive:latest from OCI archive
                                                     May be in .tar or .tar.gz format

                     Additionally, the following URIs are also supported for setups
                     that do not use a HUB or similar.  Recommend using 'checksum'!

                     ftp://addr/path/to/archive   -- Downloaded using wget
                     http://addr/path/to/archive  -- Downloaded using curl
                     https://addr/path/to/archive -- Downloaded using curl

                     Note: if a remote repository cannot be reached, the creation of the
                           container will be put on a queue that retries pull every time
                           there is a route change in the host's system.";
	mandatory true;
	type image-reference;
      }

      container checksum {
	description "Checksum for ftp/http/https OCI archives.";
        choice checksum {
          leaf md5 {
            description "MD5 checksum for the archive.";
            type string {
              length "32";
              pattern "[a-fA-F0-9]{32}";
            }
          }

          leaf sha256 {
            description "SHA256 checksum for the archive.";
            type string {
              length "64";
              pattern "[a-fA-F0-9]{64}";
            }
          }

          leaf sha512 {
            description "SHA512 checksum for the archive.";
            type string {
              length "128";
              pattern "[a-fA-F0-9]{128}";
            }
          }
        }
      }

      leaf image-id {
        description "Docker image ID, exact hash used.";
        config false;
        type string;
      }

      list env {
	description "Set environment variables, key=\"value\" pairs.";
	key key;

	leaf key {
	  description "Single word.";
	  type string {
            pattern '[a-zA-Z_][a-zA-Z0-9_]*';
            length "1..253";
          }
	}

	leaf value {
	  description "Argument to key can be a single word or quoted multiple words.";
	  mandatory true;
	  type string;
	}
      }

      leaf command {
	description "Override ENTRYPOINT from image and run command + args.";
	type string {
	  pattern '[a-zA-Z0-9_./ :=@%^,+-]+';
	}
      }

      leaf hostname {
	description "Sets the container host name, available inside the container.

                     Format specifiers are for, default hostname, ID, and the
                     last three octets in base MAC, e.g., c0-ff-ee";
	type infix-sys:hostname;
      }

      leaf privileged {
        description "Give container extended privileges, e.g., access to devices.";
        type boolean;
      }

      leaf restart-policy {
	description "Restart policy to when containers exit/crash.";
	type restart-policy;
	default always;
      }

      leaf manual {
	description "Auto-start or manual start after creation/reboot.";
	type boolean;
      }

      container network {
	description "Select network mode: none, host, or container network interfaces.";

        leaf host {
          description "Run in same network namespace as host, share DNS and publish all ports.";
          type boolean;
        }

        list interface {
          description "Container network interface(s) to connect to the container.";
          key name;

          leaf name {
            description "Container network (interface name) to connect to the container.";
            type if:interface-ref;
            must "/if:interfaces/if:interface[if:name = current()]/infix-if:container-network" {
              error-message "Container networks must be interfaces classified as container-network.";
            }
          }

          leaf-list option {
            description "Options for interface inside container, e.g., interface_name=foo0

                         Options: ip=1.2.3.4                      -- Set static IPv4 address*
                                  ip6=2001:0:130f::9c0:876a:130b  -- Set static IPv6 address*
                                  mac=00:01:02:c0:ff:ee           -- Set fixed MAC address
                                  interface_name=foo0             -- Set interface name

                         NOTE: Some (*) options only work with masquerading container bridges,
                               which automatically create VETH pairs with one end in the host
                               bridge and the other in the container.";
            type string {
              pattern '[a-zA-Z_][a-zA-Z0-9_]*=[a-zA-Z0-9.:_/-]+';
            }
          }
        }

        leaf-list publish {
          description "Publish container port, or a range of ports, to the host.

                         Syntax: [[ip:][hostPort]:]containerPort[/protocol]

                         Sample: 8080:80           -- forward tcp port 8080 to container port 80
                                 69:69/udp         -- forward udp port 69 to container port 69
                                 127.0.0.1:8080:80 -- forward only from loopback interface";
          type string {
            pattern '[0-9.:\[\]]+(/[a-zA-Z]+)?';
          }
        }

        leaf-list dns {
          description "Set custom DNS servers, or 'none' to use /etc/resolv.conf in image.";
          type inet:ip-address;
        }

        leaf-list search {
          description "Set custom DNS search domains, or '.' to not set search domain.";
          type inet:domain-name;
        }

        must "(host and not(interface)) or (not(host) and interface) or (not(host) and not(interface))" {
          error-message "Host and interfaces are mutually exclusive";
        }
      }

      leaf read-only {
        status deprecated; // This is now the default, setting kept only to not break configs
	description "Create a read-only container.  Use volumes for writable directories.";
	type boolean;
      }

      container capabilities {
	description "Capabilities to add for unprivileged and drop for privileged containers.";

	leaf-list add {
	  type capabilities;
	  description "List of capabilities to add to (an unprivileged) container.";
	}

	leaf-list drop {
	  type capabilities;
	  description "List of capabilities to drop from (a privileged) container.";
	}
      }

      container resource-limit {
	description "Resource limits for the container.";

	leaf memory {
	  description "Maximum memory limit in kibibytes, default: unlimited.";
	  type uint64;
	  units "KiB";
	}

	leaf cpu {
	  description "CPU limit in millicores, default: unlimited.

                       Millicores represent thousandths of a CPU core:
                         500  = 0.5 cores (50% of one core)
                         1000 = 1.0 cores (one full core)
                         2000 = 2.0 cores (two full cores)
                         3500 = 3.5 cores

                       This is converted to cgroup cpu.quota internally.";
	  type uint32;
	  units "millicores";
	}
      }

      container resource-usage {
	description "Runtime container resource usage statistics.";
	config false;

	leaf memory {
	  description "Used memory in kibibytes.";
	  type uint64;
	  units "KiB";
	}

	leaf cpu {
	  description "CPU usage percentage.";
	  type decimal64 {
	    fraction-digits 2;
	  }
	  units "percent";
	}

	container block-io {
	  description "Block I/O statistics";

	  leaf read {
	    description "Block I/O read in kibibytes.";
	    type uint64;
	    units "KiB";
	  }

	  leaf write {
	    description "Block I/O write in kibibytes.";
	    type uint64;
	    units "KiB";
	  }
	}

	container net-io {
	  description "Network I/O statistics";

	  leaf received {
	    description "Network I/O received in kibibytes.";
	    type uint64;
	    units "KiB";
	  }

	  leaf sent {
	    description "Network I/O sent in kibibytes.";
	    type uint64;
	    units "KiB";
	  }
	}

	leaf pids {
	  description "Number of processes/threads.";
	  type uint32;
	}
      }

      list mount {
        description "Files, content, and directories to mount inside container.";
        key name;

        leaf name {
          description "Unique name to identify mount, e.g., 'ntpd.conf' or 'leds'.

                       Set the source path or *content* to mount in the container,
                       the latter means the file contents are stored in the host's
                       startup-config, base64 encoded.

                       Example source paths:
                         1. /etc/ntpd.conf    File name to bind mount to 'path'
                         2. /sys/class/leds/  Control LEDs from a container
                         3. /dev/sda*         Share all matching files (type glob!)

                       For persistent writable directories, *volumes* may be a
                       better fit for your container and easier to set up.";
          type ident;
        }

        leaf type {
          description "Mount type, strict bind mount or glob match.";
          type mount-type;
          default bind;
        }

        choice data {
          mandatory true;
          case source {
            leaf source {
              description "Host path to mount in container, may be a glob.

                       When mounting files, directories (and globs) from the host,
                       the source must be an absolute path.";
              type string {
                pattern '/[a-zA-Z0-9_./*?-]+([ ]?[a-zA-Z0-9_./*?-]+)*';
              }
            }
          }
          case content {
            leaf content {
              description "File contents, in base64 native format (XML/JSON).

                       With this setting the source path is not used, instead
                       a temporary read-only file is created on the host with
                       this content and bind-mounted into the container at the
                       target destination path.

                       In the CLI, use 'set content' without and argument to
                       open an editor for easy copy-paste.  On exit from the
                       editor the contents are base64 encoded automatically.";
              type binary;
            }
          }
        }

        leaf target {
          description "Absolute path to target destination inside container.

                       For example, to mount file on /etc/ntpd.conf set this path and
                       then use either the 'source' path to a file on the host system,
                       or import the text file using the 'content' node.";
          mandatory true;
          type string {
            pattern '/[a-zA-Z0-9_./-]+(/[a-zA-Z0-9_./-]+)*';
          }
        }

        leaf mode {
          description "File permissions for content mounts (not used for source mounts).

                       Octal notation (e.g., '755', '0644', '4755').  When not specified,
                       the mode will be '0644'.";
          type string {
            pattern '0?[0-7]{3,4}';
          }
        }

        leaf read-only {
          description "All mounts are read-only by default.
                       Use this option to allow containers to write to files
                       and directories on the host system.

                       NOTE: 'content' files can also be set read-write, but
                             no changes are saved back to hosts's datastore.";
          type boolean;
          default true;
        }
      }

      list volume {
	description "Create a writable volume that survive container upgrades.";
	key name;

	leaf name {
	  description "Single word to identify this (named) volume.

                       Volumes are retained over the lifetime of a container and
                       survive both upgrading the image and configuration changes,
                       which otherwise wipe the default writable layer a container
                       is given.

                       Volumes combine well with 'read-only' containers, when you
                       know which files/directories you want to persist.

                       Compared to a bind mount, a volume is automatically 'synced'
                       with the contents of the container's file system on first
                       use.  Hence, upgrading the container image will not update
                       the volume if the image has new/removed files at 'path'.";
	  type ident;
	}

	leaf target {
	  description "Absolute path to target destination directory inside the container.";
	  mandatory true;
          type string {
            pattern '/[a-zA-Z0-9_./-]+(/[a-zA-Z0-9_./-]+)*';
          }
	}
      }

      leaf running {
        description "Status of container, running or not.";
        config false;
        type boolean;
      }

      leaf status {
        description "Status of container, human friendly.";
        config false;
        type string;
      }

      action start {
        description "Start a stopped container.";
      }

      action stop {
        description "Stop a running container.";
      }

      action restart {
        description "Restart a running, or start, a stopped container.";
      }

      action upgrade {
        description "Upgrade container to latest version of its image.

                     This action fetches the latest version of the container's image
                     and recreates the container with the new image. Any named volumes
                     are preserved.

                     Note: This action is primarily for containers using mutable tags
                           like ':latest'.  For containers using immutable tags (e.g.,
                           ':25.06.0'), the upgrade action will have no effect.";
      }
    }
  }

  rpc oci-load {
    description "Load an OCI archive from file or URL to an image.";
    input {
      leaf uri {
        description "The URL or local file path, e.g., /lib/oci/archive.tar.gz";
        type image-reference;
        mandatory true;
      }
      leaf name {
        description "Image name[:tag], default: basename of archive dir + :latest";
        type image-reference;
      }
    }
  }
}
