/*
 * 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 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 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 string;
      }

      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 string;
      }

      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;
      }

      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;
          }
        }

        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;
        }

        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.";
	}
      }

      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 string;
        }

        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 '/.*';
              }
            }
          }
          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 '/.*';
          }
        }

        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 string {
            pattern '[a-zA-Z_][a-zA-Z0-9_]*';
            length "1..64";
          }
	}

	leaf target {
	  description "Absolute path to target destination directory inside the container.";
	  mandatory true;
          type string {
            pattern '/.*';
          }
	}
      }

      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.";
      }
    }
  }

  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 string;
        mandatory true;
      }
      leaf name {
        description "Image name[:tag], default: basename of archive dir + :latest";
        type string;
      }
    }
  }
}
