diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 244106a03..4916ffb61 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -116,7 +116,9 @@ jobs: else DCO_RANGE=$(curl ${GITHUB_COMMIT_URL} | jq -r '.[0].parents[0].sha + "..HEAD"') fi - ../project/script/validate/dco + # Add back after CRI merge complete and remove last call + # ../project/script/validate/dco + echo "skipping for CRI merge since older commits may not pass this check" - name: Headers run: ../project/script/validate/fileheader ../project/ diff --git a/Makefile b/Makefile index adac53e86..a7906f8e2 100644 --- a/Makefile +++ b/Makefile @@ -85,7 +85,7 @@ GO_LDFLAGS=-ldflags '-X $(PKG)/version.Version=$(VERSION) -X $(PKG)/version.Revi SHIM_GO_LDFLAGS=-ldflags '-X $(PKG)/version.Version=$(VERSION) -X $(PKG)/version.Revision=$(REVISION) -X $(PKG)/version.Package=$(PACKAGE) -extldflags "-static" $(EXTRA_LDFLAGS)' # Project packages. -PACKAGES=$(shell go list ${GO_TAGS} ./... | grep -v /vendor/) +PACKAGES=$(shell go list ${GO_TAGS} ./... | grep -v /vendor/ | grep -v /integration) INTEGRATION_PACKAGE=${PKG} TEST_REQUIRES_ROOT_PACKAGES=$(filter \ ${PACKAGES}, \ diff --git a/cmd/containerd/builtins_cri.go b/cmd/containerd/builtins_cri.go index 2abc24e3a..4d5129d0d 100644 --- a/cmd/containerd/builtins_cri.go +++ b/cmd/containerd/builtins_cri.go @@ -18,4 +18,4 @@ package main -import _ "github.com/containerd/cri" +import _ "github.com/containerd/containerd/pkg/cri" diff --git a/contrib/ansible/README.md b/contrib/ansible/README.md new file mode 100644 index 000000000..bbe05d3ff --- /dev/null +++ b/contrib/ansible/README.md @@ -0,0 +1,122 @@ +# Kubernetes Cluster with Containerd +

+ + +

+ + +This document provides the steps to bring up a Kubernetes cluster using ansible and kubeadm tools. + +### Prerequisites: +- **OS**: Ubuntu 16.04 (will be updated with additional distros after testing) +- **Python**: 2.7+ +- **Ansible**: 2.4+ + +## Step 0: +- Install Ansible on the host where you will provision the cluster. This host may be one of the nodes you plan to include in your cluster. Installation instructions for Ansible are found [here](http://docs.ansible.com/ansible/latest/intro_installation.html). +- Create a hosts file and include the IP addresses of the hosts that need to be provisioned by Ansible. +```console +$ cat hosts +172.31.7.230 +172.31.13.159 +172.31.1.227 +``` +- Setup passwordless SSH access from the host where you are running Ansible to all the hosts in the hosts file. The instructions can be found in [here](http://www.linuxproblem.org/art_9.html) + +## Step 1: +At this point, the ansible playbook should be able to ssh into the machines in the hosts file. +```console +git clone https://github.com/containerd/cri +cd ./cri/contrib/ansible +ansible-playbook -i hosts cri-containerd.yaml +``` +A typical cloud login might have a username and private key file, in which case the following can be used: +```console +ansible-playbook -i hosts -u --private-key cri-containerd.yaml + ``` +For more options ansible config file (/etc/ansible/ansible.cfg) can be used to set defaults. Please refer to [Ansible options](http://docs.ansible.com/ansible/latest/intro_configuration.html) for advanced ansible configurations. + +At the end of this step, you will have the required software installed in the hosts to bringup a kubernetes cluster. +```console +PLAY RECAP *************************************************************************************************************************************************************** +172.31.1.227 : ok=21 changed=7 unreachable=0 failed=0 +172.31.13.159 : ok=21 changed=7 unreachable=0 failed=0 +172.31.7.230 : ok=21 changed=7 unreachable=0 failed=0 +``` + +## Step 2: +Use [kubeadm](https://kubernetes.io/docs/setup/independent/install-kubeadm/) to bring up a Kubernetes Cluster. Depending on what third-party provider you choose, you might have to set the ```--pod-network-cidr``` to something provider-specific. +Initialize the cluster from one of the nodes (Note: This node will be the master node): +```console +$sudo kubeadm init --skip-preflight-checks +[kubeadm] WARNING: kubeadm is in beta, please do not use it for production clusters. +[init] Using Kubernetes version: v1.7.6 +[init] Using Authorization modes: [Node RBAC] +[preflight] Skipping pre-flight checks +[kubeadm] WARNING: starting in 1.8, tokens expire after 24 hours by default (if you require a non-expiring token use --token-ttl 0) +[certificates] Generated CA certificate and key. +[certificates] Generated API server certificate and key. +[certificates] API Server serving cert is signed for DNS names [abhi-k8-ubuntu-1 kubernetes kubernetes.default kubernetes.default.svc kubernetes.default.svc.cluster.local] and IPs [10.96.0.1 172.31.7.230] +[certificates] Generated API server kubelet client certificate and key. +[certificates] Generated service account token signing key and public key. +[certificates] Generated front-proxy CA certificate and key. +[certificates] Generated front-proxy client certificate and key. +[certificates] Valid certificates and keys now exist in "/etc/kubernetes/pki" +[kubeconfig] Wrote KubeConfig file to disk: "/etc/kubernetes/admin.conf" +[kubeconfig] Wrote KubeConfig file to disk: "/etc/kubernetes/kubelet.conf" +[kubeconfig] Wrote KubeConfig file to disk: "/etc/kubernetes/controller-manager.conf" +[kubeconfig] Wrote KubeConfig file to disk: "/etc/kubernetes/scheduler.conf" +[apiclient] Created API client, waiting for the control plane to become ready +[apiclient] All control plane components are healthy after 42.002391 seconds +[token] Using token: 43a25d.420ff2e06336e4c1 +[apiconfig] Created RBAC rules +[addons] Applied essential addon: kube-proxy +[addons] Applied essential addon: kube-dns + +Your Kubernetes master has initialized successfully! + +To start using your cluster, you need to run (as a regular user): + + mkdir -p $HOME/.kube + sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config + sudo chown $(id -u):$(id -g) $HOME/.kube/config + +You should now deploy a pod network to the cluster. +Run "kubectl apply -f [podnetwork].yaml" with one of the options listed at: + http://kubernetes.io/docs/admin/addons/ + +You can now join any number of machines by running the following on each node +as root: + + kubeadm join --token 43a25d.420ff2e06336e4c1 172.31.7.230:6443 + +``` +## Step 3: +Use kubeadm join to add each of the remaining nodes to your cluster. (Note: Uses token that was generated during cluster init.) +```console +$sudo kubeadm join --token 43a25d.420ff2e06336e4c1 172.31.7.230:6443 --skip-preflight-checks +[kubeadm] WARNING: kubeadm is in beta, please do not use it for production clusters. +[preflight] Skipping pre-flight checks +[discovery] Trying to connect to API Server "172.31.7.230:6443" +[discovery] Created cluster-info discovery client, requesting info from "https://172.31.7.230:6443" +[discovery] Cluster info signature and contents are valid, will use API Server "https://172.31.7.230:6443" +[discovery] Successfully established connection with API Server "172.31.7.230:6443" +[bootstrap] Detected server version: v1.7.6 +[bootstrap] The server supports the Certificates API (certificates.k8s.io/v1beta1) +[csr] Created API client to obtain unique certificate for this node, generating keys and certificate signing request +[csr] Received signed certificate from the API server, generating KubeConfig... +[kubeconfig] Wrote KubeConfig file to disk: "/etc/kubernetes/kubelet.conf" + +Node join complete: +* Certificate signing request sent to master and response + received. +* Kubelet informed of new secure connection details. + +Run 'kubectl get nodes' on the master to see this machine join. +``` +At the end of Step 3 you should have a kubernetes cluster up and running and ready for deployment. + +## Step 4: +Please follow the instructions [here](https://kubernetes.io/docs/setup/independent/create-cluster-kubeadm/#pod-network) to deploy CNI network plugins and start a demo app. + +We are constantly striving to improve the installer. Please feel free to open issues and provide suggestions to make the installer fast and easy to use. We are open to receiving help in validating and improving the installer on different distros. diff --git a/contrib/ansible/cri-containerd.yaml b/contrib/ansible/cri-containerd.yaml new file mode 100644 index 000000000..f7949601b --- /dev/null +++ b/contrib/ansible/cri-containerd.yaml @@ -0,0 +1,66 @@ +--- +- hosts: all + become: true + tasks: + - include_vars: vars/vars.yaml # Contains tasks variables for installer + - include_tasks: tasks/bootstrap_ubuntu.yaml # Contains tasks bootstrap components for ubuntu systems + when: ansible_distribution == "Ubuntu" + - include_tasks: tasks/bootstrap_centos.yaml # Contains tasks bootstrap components for centos systems + when: ansible_distribution == "CentOS" + - include_tasks: tasks/k8s.yaml # Contains tasks kubernetes component installation + - include_tasks: tasks/binaries.yaml # Contains tasks for pulling containerd components + + - name: "Create a directory for containerd config" + file: path=/etc/containerd state=directory + + - name: "Start Containerd" + systemd: name=containerd daemon_reload=yes state=started enabled=yes + + - name: "Load br_netfilter kernel module" + modprobe: + name: br_netfilter + state: present + + - name: "Set bridge-nf-call-iptables" + sysctl: + name: net.bridge.bridge-nf-call-iptables + value: 1 + + - name: "Set ip_forward" + sysctl: + name: net.ipv4.ip_forward + value: 1 + + - name: "Check kubelet args in kubelet config (Ubuntu)" + shell: grep "^Environment=\"KUBELET_EXTRA_ARGS=" /etc/systemd/system/kubelet.service.d/10-kubeadm.conf || true + register: check_args + when: ansible_distribution == "Ubuntu" + + - name: "Add runtime args in kubelet conf (Ubuntu)" + lineinfile: + dest: "/etc/systemd/system/kubelet.service.d/10-kubeadm.conf" + line: "Environment=\"KUBELET_EXTRA_ARGS= --runtime-cgroups=/system.slice/containerd.service --container-runtime=remote --runtime-request-timeout=15m --container-runtime-endpoint=unix:///run/containerd/containerd.sock\"" + insertafter: '\[Service\]' + when: ansible_distribution == "Ubuntu" and check_args.stdout == "" + + - name: "Check kubelet args in kubelet config (CentOS)" + shell: grep "^Environment=\"KUBELET_EXTRA_ARGS=" /usr/lib/systemd/system/kubelet.service.d/10-kubeadm.conf || true + register: check_args + when: ansible_distribution == "CentOS" + + - name: "Add runtime args in kubelet conf (CentOS)" + lineinfile: + dest: "/usr/lib/systemd/system/kubelet.service.d/10-kubeadm.conf" + line: "Environment=\"KUBELET_EXTRA_ARGS= --runtime-cgroups=/system.slice/containerd.service --container-runtime=remote --runtime-request-timeout=15m --container-runtime-endpoint=unix:///run/containerd/containerd.sock\"" + insertafter: '\[Service\]' + when: ansible_distribution == "CentOS" and check_args.stdout == "" + + - name: "Start Kubelet" + systemd: name=kubelet daemon_reload=yes state=started enabled=yes + + # TODO This needs to be removed once we have consistent concurrent pull results + - name: "Pre-pull pause container image" + shell: | + /usr/local/bin/ctr pull k8s.gcr.io/pause:3.2 + /usr/local/bin/crictl --runtime-endpoint unix:///run/containerd/containerd.sock \ + pull k8s.gcr.io/pause:3.2 diff --git a/contrib/ansible/tasks/binaries.yaml b/contrib/ansible/tasks/binaries.yaml new file mode 100644 index 000000000..b34144f75 --- /dev/null +++ b/contrib/ansible/tasks/binaries.yaml @@ -0,0 +1,12 @@ +--- +- name: "Get Containerd" + unarchive: + src: "https://storage.googleapis.com/cri-containerd-release/cri-containerd-{{ containerd_release_version }}.linux-amd64.tar.gz" + dest: "/" + remote_src: yes + +- name: "Create a directory for cni binary" + file: path={{ cni_bin_dir }} state=directory + +- name: "Create a directory for cni config files" + file: path={{ cni_conf_dir }} state=directory diff --git a/contrib/ansible/tasks/bootstrap_centos.yaml b/contrib/ansible/tasks/bootstrap_centos.yaml new file mode 100644 index 000000000..5d9e66a62 --- /dev/null +++ b/contrib/ansible/tasks/bootstrap_centos.yaml @@ -0,0 +1,12 @@ +--- +- name: "Install required packages on CentOS " + yum: + name: "{{ item }}" + state: latest + with_items: + - unzip + - tar + - btrfs-progs + - libseccomp + - util-linux + - libselinux-python diff --git a/contrib/ansible/tasks/bootstrap_ubuntu.yaml b/contrib/ansible/tasks/bootstrap_ubuntu.yaml new file mode 100644 index 000000000..3bb9b2134 --- /dev/null +++ b/contrib/ansible/tasks/bootstrap_ubuntu.yaml @@ -0,0 +1,12 @@ +--- +- name: "Install required packages on Ubuntu" + package: + name: "{{ item }}" + state: latest + with_items: + - unzip + - tar + - apt-transport-https + - btrfs-tools + - libseccomp2 + - util-linux diff --git a/contrib/ansible/tasks/k8s.yaml b/contrib/ansible/tasks/k8s.yaml new file mode 100644 index 000000000..e2e017c20 --- /dev/null +++ b/contrib/ansible/tasks/k8s.yaml @@ -0,0 +1,52 @@ +--- +- name: "Add gpg key (Ubuntu)" + apt_key: + url: https://packages.cloud.google.com/apt/doc/apt-key.gpg + state: present + when: ansible_distribution == "Ubuntu" + +- name: "Add kubernetes source list (Ubuntu)" + apt_repository: + repo: "deb http://apt.kubernetes.io/ kubernetes-{{ ansible_distribution_release }} main" + state: present + filename: "kubernetes" + when: ansible_distribution == "Ubuntu" + +- name: "Update the repository cache (Ubuntu)" + apt: + update_cache: yes + when: ansible_distribution == "Ubuntu" + +- name: "Add Kubernetes repository and install gpg key (CentOS)" + yum_repository: + name: kubernetes + description: Kubernetes repository + baseurl: https://packages.cloud.google.com/yum/repos/kubernetes-el7-x86_64 + gpgcheck: yes + enabled: yes + repo_gpgcheck: yes + gpgkey: + - https://packages.cloud.google.com/yum/doc/yum-key.gpg + - https://packages.cloud.google.com/yum/doc/rpm-package-key.gpg + when: ansible_distribution == "CentOS" + +- name: "Disable SELinux (CentOS)" + selinux: + state: disabled + when: ansible_distribution == "CentOS" + +- name: "Install kubelet,kubeadm,kubectl (CentOS)" + yum: state=present name={{ item }} + with_items: + - kubelet + - kubeadm + - kubectl + when: ansible_distribution == "CentOS" + +- name: "Install kubelet, kubeadm, kubectl (Ubuntu)" + apt: name={{item}} state=installed + with_items: + - kubelet + - kubeadm + - kubectl + when: ansible_distribution == "Ubuntu" diff --git a/contrib/ansible/vars/vars.yaml b/contrib/ansible/vars/vars.yaml new file mode 100644 index 000000000..9ae0e0680 --- /dev/null +++ b/contrib/ansible/vars/vars.yaml @@ -0,0 +1,4 @@ +--- +containerd_release_version: 1.3.0 +cni_bin_dir: /opt/cni/bin/ +cni_conf_dir: /etc/cni/net.d/ diff --git a/contrib/linuxkit/README.md b/contrib/linuxkit/README.md new file mode 100644 index 000000000..18dada82f --- /dev/null +++ b/contrib/linuxkit/README.md @@ -0,0 +1,5 @@ +# LinuxKit Kubernetes project + +The LinuxKit [`projects/kubernetes`](https://github.com/linuxkit/linuxkit/tree/master/projects/kubernetes) subdirectory contains a project to build master and worker node virtual machines. When built with `KUBE_RUNTIME=cri-containerd` then these images will use `cri-containerd` as their execution backend. + +See the [project README](https://github.com/linuxkit/linuxkit/blob/master/projects/kubernetes/README.md). diff --git a/contrib/systemd-units/containerd.service b/contrib/systemd-units/containerd.service new file mode 100644 index 000000000..c059e97ae --- /dev/null +++ b/contrib/systemd-units/containerd.service @@ -0,0 +1,22 @@ +[Unit] +Description=containerd container runtime +Documentation=https://containerd.io +After=network.target + +[Service] +ExecStartPre=/sbin/modprobe overlay +ExecStart=/usr/local/bin/containerd +Restart=always +RestartSec=5 +Delegate=yes +KillMode=process +OOMScoreAdjust=-999 +LimitNOFILE=1048576 +# Having non-zero Limit*s causes performance problems due to accounting overhead +# in the kernel. We recommend using cgroups to do container-local accounting. +LimitNPROC=infinity +LimitCORE=infinity +TasksMax=infinity + +[Install] +WantedBy=multi-user.target diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 000000000..823e72853 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,18 @@ +# Architecture of The CRI Plugin +This document describes the architecture of the `cri` plugin for `containerd`. + +This plugin is an implementation of Kubernetes [container runtime interface (CRI)](https://github.com/kubernetes/kubernetes/blob/master/pkg/kubelet/apis/cri/runtime/v1alpha2/api.proto). Containerd operates on the same node as the [Kubelet](https://kubernetes.io/docs/reference/generated/kubelet/). The `cri` plugin inside containerd handles all CRI service requests from the Kubelet and uses containerd internals to manage containers and container images. + +The `cri` plugin uses containerd to manage the full container lifecycle and all container images. As also shown below, `cri` manages pod networking via [CNI](https://github.com/containernetworking/cni) (another CNCF project). + +![architecture](./architecture.png) + +Let's use an example to demonstrate how the `cri` plugin works for the case when Kubelet creates a single-container pod: +* Kubelet calls the `cri` plugin, via the CRI runtime service API, to create a pod; +* `cri` creates and configures the pod’s network namespace using CNI; +* `cri` uses containerd internal to create and start a special [pause container](https://www.ianlewis.org/en/almighty-pause-container) (the sandbox container) and put that container inside the pod’s cgroups and namespace (steps omitted for brevity); +* Kubelet subsequently calls the `cri` plugin, via the CRI image service API, to pull the application container image; +* `cri` further uses containerd to pull the image if the image is not present on the node; +* Kubelet then calls `cri`, via the CRI runtime service API, to create and start the application container inside the pod using the pulled container image; +* `cri` finally uses containerd internal to create the application container, put it inside the pod’s cgroups and namespace, then to start the pod’s new application container. +After these steps, a pod and its corresponding application container is created and running. diff --git a/docs/architecture.png b/docs/architecture.png new file mode 100644 index 000000000..c65bd8e87 Binary files /dev/null and b/docs/architecture.png differ diff --git a/docs/config.md b/docs/config.md new file mode 100644 index 000000000..1203c24c7 --- /dev/null +++ b/docs/config.md @@ -0,0 +1,316 @@ +# CRI Plugin Config Guide +This document provides the description of the CRI plugin configuration. +The CRI plugin config is part of the containerd config (default +path: `/etc/containerd/config.toml`). + +See [here](https://github.com/containerd/containerd/blob/master/docs/ops.md) +for more information about containerd config. + +The explanation and default value of each configuration item are as follows: +```toml +# Use config version 2 to enable new configuration fields. +# Config file is parsed as version 1 by default. +# Version 2 uses long plugin names, i.e. "io.containerd.grpc.v1.cri" vs "cri". +version = 2 + +# The 'plugins."io.containerd.grpc.v1.cri"' table contains all of the server options. +[plugins."io.containerd.grpc.v1.cri"] + + # disable_tcp_service disables serving CRI on the TCP server. + # Note that a TCP server is enabled for containerd if TCPAddress is set in section [grpc]. + disable_tcp_service = true + + # stream_server_address is the ip address streaming server is listening on. + stream_server_address = "127.0.0.1" + + # stream_server_port is the port streaming server is listening on. + stream_server_port = "0" + + # stream_idle_timeout is the maximum time a streaming connection can be + # idle before the connection is automatically closed. + # The string is in the golang duration format, see: + # https://golang.org/pkg/time/#ParseDuration + stream_idle_timeout = "4h" + + # enable_selinux indicates to enable the selinux support. + enable_selinux = false + + # selinux_category_range allows the upper bound on the category range to be set. + # if not specified or set to 0, defaults to 1024 from the selinux package. + selinux_category_range = 1024 + + # sandbox_image is the image used by sandbox container. + sandbox_image = "k8s.gcr.io/pause:3.2" + + # stats_collect_period is the period (in seconds) of snapshots stats collection. + stats_collect_period = 10 + + # enable_tls_streaming enables the TLS streaming support. + # It generates a self-sign certificate unless the following x509_key_pair_streaming are both set. + enable_tls_streaming = false + + # tolerate_missing_hugetlb_controller if set to false will error out on create/update + # container requests with huge page limits if the cgroup controller for hugepages is not present. + # This helps with supporting Kubernetes <=1.18 out of the box. (default is `true`) + tolerate_missing_hugetlb_controller = true + + # ignore_image_defined_volumes ignores volumes defined by the image. Useful for better resource + # isolation, security and early detection of issues in the mount configuration when using + # ReadOnlyRootFilesystem since containers won't silently mount a temporary volume. + ignore_image_defined_volumes = false + + # 'plugins."io.containerd.grpc.v1.cri".x509_key_pair_streaming' contains a x509 valid key pair to stream with tls. + [plugins."io.containerd.grpc.v1.cri".x509_key_pair_streaming] + # tls_cert_file is the filepath to the certificate paired with the "tls_key_file" + tls_cert_file = "" + + # tls_key_file is the filepath to the private key paired with the "tls_cert_file" + tls_key_file = "" + + # max_container_log_line_size is the maximum log line size in bytes for a container. + # Log line longer than the limit will be split into multiple lines. -1 means no + # limit. + max_container_log_line_size = 16384 + + # disable_cgroup indicates to disable the cgroup support. + # This is useful when the daemon does not have permission to access cgroup. + disable_cgroup = false + + # disable_apparmor indicates to disable the apparmor support. + # This is useful when the daemon does not have permission to access apparmor. + disable_apparmor = false + + # restrict_oom_score_adj indicates to limit the lower bound of OOMScoreAdj to + # the containerd's current OOMScoreAdj. + # This is useful when the containerd does not have permission to decrease OOMScoreAdj. + restrict_oom_score_adj = false + + # max_concurrent_downloads restricts the number of concurrent downloads for each image. + max_concurrent_downloads = 3 + + # disable_proc_mount disables Kubernetes ProcMount support. This MUST be set to `true` + # when using containerd with Kubernetes <=1.11. + disable_proc_mount = false + + # unsetSeccompProfile is the profile containerd/cri will use if the provided seccomp profile is + # unset (`""`) for a container (default is `unconfined`) + unset_seccomp_profile = "" + + # 'plugins."io.containerd.grpc.v1.cri".containerd' contains config related to containerd + [plugins."io.containerd.grpc.v1.cri".containerd] + + # snapshotter is the snapshotter used by containerd. + snapshotter = "overlayfs" + + # no_pivot disables pivot-root (linux only), required when running a container in a RamDisk with runc. + # This only works for runtime type "io.containerd.runtime.v1.linux". + no_pivot = false + + # disable_snapshot_annotations disables to pass additional annotations (image + # related information) to snapshotters. These annotations are required by + # stargz snapshotter (https://github.com/containerd/stargz-snapshotter) + disable_snapshot_annotations = false + + # discard_unpacked_layers allows GC to remove layers from the content store after + # successfully unpacking these layers to the snapshotter. + discard_unpacked_layers = false + + # default_runtime_name is the default runtime name to use. + default_runtime_name = "runc" + + # 'plugins."io.containerd.grpc.v1.cri".containerd.default_runtime' is the runtime to use in containerd. + # DEPRECATED: use `default_runtime_name` and `plugins."io.containerd.grpc.v1.cri".runtimes` instead. + # Remove in containerd 1.4. + [plugins."io.containerd.grpc.v1.cri".containerd.default_runtime] + + # 'plugins."io.containerd.grpc.v1.cri".containerd.untrusted_workload_runtime' is a runtime to run untrusted workloads on it. + # DEPRECATED: use `untrusted` runtime in `plugins."io.containerd.grpc.v1.cri".runtimes` instead. + # Remove in containerd 1.4. + [plugins."io.containerd.grpc.v1.cri".containerd.untrusted_workload_runtime] + + # 'plugins."io.containerd.grpc.v1.cri".containerd.runtimes' is a map from CRI RuntimeHandler strings, which specify types + # of runtime configurations, to the matching configurations. + # In this example, 'runc' is the RuntimeHandler string to match. + [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc] + # runtime_type is the runtime type to use in containerd. + # The default value is "io.containerd.runc.v2" since containerd 1.4. + # The default value was "io.containerd.runc.v1" in containerd 1.3, "io.containerd.runtime.v1.linux" in prior releases. + runtime_type = "io.containerd.runc.v2" + + # pod_annotations is a list of pod annotations passed to both pod + # sandbox as well as container OCI annotations. Pod_annotations also + # supports golang path match pattern - https://golang.org/pkg/path/#Match. + # e.g. ["runc.com.*"], ["*.runc.com"], ["runc.com/*"]. + # + # For the naming convention of annotation keys, please reference: + # * Kubernetes: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/#syntax-and-character-set + # * OCI: https://github.com/opencontainers/image-spec/blob/master/annotations.md + pod_annotations = [] + + # container_annotations is a list of container annotations passed through to the OCI config of the containers. + # Container annotations in CRI are usually generated by other Kubernetes node components (i.e., not users). + # Currently, only device plugins populate the annotations. + container_annotations = [] + + # privileged_without_host_devices allows overloading the default behaviour of passing host + # devices through to privileged containers. This is useful when using a runtime where it does + # not make sense to pass host devices to the container when privileged. Defaults to false - + # i.e pass host devices through to privileged containers. + privileged_without_host_devices = false + + # base_runtime_spec is a file path to a JSON file with the OCI spec that will be used as the base spec that all + # container's are created from. + # Use containerd's `ctr oci spec > /etc/containerd/cri-base.json` to output initial spec file. + # Spec files are loaded at launch, so containerd daemon must be restared on any changes to refresh default specs. + # Still running containers and restarted containers will still be using the original spec from which that container was created. + base_runtime_spec = "" + + # 'plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options' is options specific to + # "io.containerd.runc.v1" and "io.containerd.runc.v2". Its corresponding options type is: + # https://github.com/containerd/containerd/blob/v1.3.2/runtime/v2/runc/options/oci.pb.go#L26 . + [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options] + # NoPivotRoot disables pivot root when creating a container. + NoPivotRoot = false + + # NoNewKeyring disables new keyring for the container. + NoNewKeyring = false + + # ShimCgroup places the shim in a cgroup. + ShimCgroup = "" + + # IoUid sets the I/O's pipes uid. + IoUid = 0 + + # IoGid sets the I/O's pipes gid. + IoGid = 0 + + # BinaryName is the binary name of the runc binary. + BinaryName = "" + + # Root is the runc root directory. + Root = "" + + # CriuPath is the criu binary path. + CriuPath = "" + + # SystemdCgroup enables systemd cgroups. + SystemdCgroup = false + + # CriuImagePath is the criu image path + CriuImagePath = "" + + # CriuWorkPath is the criu work path. + CriuWorkPath = "" + + # 'plugins."io.containerd.grpc.v1.cri".cni' contains config related to cni + [plugins."io.containerd.grpc.v1.cri".cni] + # bin_dir is the directory in which the binaries for the plugin is kept. + bin_dir = "/opt/cni/bin" + + # conf_dir is the directory in which the admin places a CNI conf. + conf_dir = "/etc/cni/net.d" + + # max_conf_num specifies the maximum number of CNI plugin config files to + # load from the CNI config directory. By default, only 1 CNI plugin config + # file will be loaded. If you want to load multiple CNI plugin config files + # set max_conf_num to the number desired. Setting max_config_num to 0 is + # interpreted as no limit is desired and will result in all CNI plugin + # config files being loaded from the CNI config directory. + max_conf_num = 1 + + # conf_template is the file path of golang template used to generate + # cni config. + # If this is set, containerd will generate a cni config file from the + # template. Otherwise, containerd will wait for the system admin or cni + # daemon to drop the config file into the conf_dir. + # This is a temporary backward-compatible solution for kubenet users + # who don't have a cni daemonset in production yet. + # This will be deprecated when kubenet is deprecated. + # See the "CNI Config Template" section for more details. + conf_template = "" + + # 'plugins."io.containerd.grpc.v1.cri".registry' contains config related to the registry + [plugins."io.containerd.grpc.v1.cri".registry] + + # 'plugins."io.containerd.grpc.v1.cri.registry.headers sets the http request headers to send for all registry requests + [plugins."io.containerd.grpc.v1.cri".registry.headers] + Foo = ["bar"] + + # 'plugins."io.containerd.grpc.v1.cri".registry.mirrors' are namespace to mirror mapping for all namespaces. + [plugins."io.containerd.grpc.v1.cri".registry.mirrors] + [plugins."io.containerd.grpc.v1.cri".registry.mirrors."docker.io"] + endpoint = ["https://registry-1.docker.io", ] + + # 'plugins."io.containerd.grpc.v1.cri".image_decryption' contains config related + # to handling decryption of encrypted container images. + [plugins."io.containerd.grpc.v1.cri".image_decryption] + # key_model defines the name of the key model used for how the cri obtains + # keys used for decryption of encrypted container images. + # The [decryption document](https://github.com/containerd/cri/blob/master/docs/decryption.md) + # contains additional information about the key models available. + # + # Set of available string options: {"", "node"} + # Omission of this field defaults to the empty string "", which indicates no key model, + # disabling image decryption. + # + # In order to use the decryption feature, additional configurations must be made. + # The [decryption document](https://github.com/containerd/cri/blob/master/docs/decryption.md) + # provides information of how to set up stream processors and the containerd imgcrypt decoder + # with the appropriate key models. + # + # Additional information: + # * Stream processors: https://github.com/containerd/containerd/blob/master/docs/stream_processors.md + # * Containerd imgcrypt: https://github.com/containerd/imgcrypt + key_model = "node" +``` + +## Untrusted Workload + +The recommended way to run untrusted workload is to use +[`RuntimeClass`](https://kubernetes.io/docs/concepts/containers/runtime-class/) api +introduced in Kubernetes 1.12 to select RuntimeHandlers configured to run +untrusted workload in `plugins."io.containerd.grpc.v1.cri".containerd.runtimes`. + +However, if you are using the legacy `io.kubernetes.cri.untrusted-workload`pod annotation +to request a pod be run using a runtime for untrusted workloads, the RuntimeHandler +`plugins."io.containerd.grpc.v1.cri"cri.containerd.runtimes.untrusted` must be defined first. +When the annotation `io.kubernetes.cri.untrusted-workload` is set to `true` the `untrusted` +runtime will be used. For example, see +[Create an untrusted pod using Kata Containers](https://github.com/kata-containers/documentation/blob/master/how-to/how-to-use-k8s-with-cri-containerd-and-kata.md#create-an-untrusted-pod-using-kata-containers). + +## CNI Config Template + +Ideally the cni config should be placed by system admin or cni daemon like calico, +weaveworks etc. However, there are still users using [kubenet](https://kubernetes.io/docs/concepts/cluster-administration/network-plugins/#kubenet) +today, who don't have a cni daemonset in production. The cni config template is +a temporary backward-compatible solution for them. This is expected to be +deprecated when kubenet is deprecated. + +The cni config template uses the [golang +template](https://golang.org/pkg/text/template/) format. Currently supported +values are: +* `.PodCIDR` is a string of the first CIDR assigned to the node. +* `.PodCIDRRanges` is a string array of all CIDRs assigned to the node. It is + usually used for + [dualstack](https://github.com/kubernetes/enhancements/blob/master/keps/sig-network/20180612-ipv4-ipv6-dual-stack.md) support. +* `.Routes` is a string array of all routes needed. It is usually used for + dualstack support or single stack but IPv4 or IPv6 is decided at runtime. + +The [golang template actions](https://golang.org/pkg/text/template/#hdr-Actions) +can be used to render the cni config. For example, you can use the following +template to add CIDRs and routes for dualstack in the CNI config: +``` +"ipam": { + "type": "host-local", + "ranges": [{{range $i, $range := .PodCIDRRanges}}{{if $i}}, {{end}}[{"subnet": "{{$range}}"}]{{end}}], + "routes": [{{range $i, $route := .Routes}}{{if $i}}, {{end}}{"dst": "{{$route}}"}{{end}}] +} +``` + +## Deprecation +The config options of the CRI plugin follow the [Kubernetes deprecation +policy of "admin-facing CLI components"](https://kubernetes.io/docs/reference/using-api/deprecation-policy/#deprecating-a-flag-or-cli). + +In summary, when a config option is announced to be deprecated: +* It is kept functional for 6 months or 1 release (whichever is longer); +* A warning is emitted when it is used. diff --git a/docs/containerd.png b/docs/containerd.png new file mode 100644 index 000000000..9eb1802b8 Binary files /dev/null and b/docs/containerd.png differ diff --git a/docs/cri.png b/docs/cri.png new file mode 100644 index 000000000..0373b3bc0 Binary files /dev/null and b/docs/cri.png differ diff --git a/docs/crictl.md b/docs/crictl.md new file mode 100644 index 000000000..3a71575b5 --- /dev/null +++ b/docs/crictl.md @@ -0,0 +1,216 @@ +CRICTL User Guide +================= +This document presumes you already have `containerd` with the `cri` plugin installed and running. + +This document is for developers who wish to debug, inspect, and manage their pods, +containers, and container images. + +Before generating issues against this document, `containerd`, `containerd/cri`, +or `crictl` please make sure the issue has not already been submitted. + +## Install crictl +If you have not already installed crictl please install the version compatible +with the `cri` plugin you are using. If you are a user, your deployment +should have installed crictl for you. If not, get it from your release tarball. +If you are a developer the current version of crictl is specified [here](../hack/utils.sh). +A helper command has been included to install the dependencies at the right version: +```console +$ make install.deps +``` +* Note: The file named `/etc/crictl.yaml` is used to configure crictl +so you don't have to repeatedly specify the runtime sock used to connect crictl +to the container runtime: +```console +$ cat /etc/crictl.yaml +runtime-endpoint: unix:///run/containerd/containerd.sock +image-endpoint: unix:///run/containerd/containerd.sock +timeout: 10 +debug: true +``` + +## Download and Inspect a Container Image +The pull command tells the container runtime to download a container image from +a container registry. +```console +$ crictl pull busybox + ... +$ crictl inspecti busybox + ... displays information about the image. +``` + +***Note:*** If you get an error similar to the following when running a `crictl` +command (and your containerd instance is already running): +```console +crictl info +FATA[0000] getting status of runtime failed: rpc error: code = Unimplemented desc = unknown service runtime.v1alpha2.RuntimeService +``` +This could be that you are using an incorrect containerd configuration (maybe +from a Docker install). You will need to update your containerd configuration +to the containerd instance that you are running. One way of doing this is as +follows: +```console +$ mv /etc/containerd/config.toml /etc/containerd/config.bak +$ containerd config default > /etc/containerd/config.toml +``` + +## Directly Load a Container Image +Another way to load an image into the container runtime is with the load +command. With the load command you inject a container image into the container +runtime from a file. First you need to create a container image tarball. For +example to create an image tarball for a pause container using Docker: +```console +$ docker pull k8s.gcr.io/pause-amd64:3.2 + 3.2: Pulling from pause-amd64 + 67ddbfb20a22: Pull complete + Digest: sha256:59eec8837a4d942cc19a52b8c09ea75121acc38114a2c68b98983ce9356b8610 + Status: Downloaded newer image for k8s.gcr.io/pause-amd64:3.2 +$ docker save k8s.gcr.io/pause-amd64:3.2 -o pause.tar +``` +Then use [`ctr`](https://github.com/containerd/containerd/blob/master/docs/man/ctr.1.md) +to load the container image into the container runtime: +```console +# The cri plugin uses the "k8s.io" containerd namespace. +$ sudo ctr -n=k8s.io images import pause.tar + Loaded image: k8s.gcr.io/pause-amd64:3.2 +``` +List images and inspect the pause image: +```console +$ sudo crictl images +IMAGE TAG IMAGE ID SIZE +docker.io/library/busybox latest f6e427c148a76 728kB +k8s.gcr.io/pause-amd64 3.2 da86e6ba6ca19 746kB +$ sudo crictl inspecti da86e6ba6ca19 + ... displays information about the pause image. +$ sudo crictl inspecti k8s.gcr.io/pause-amd64:3.2 + ... displays information about the pause image. +``` + +## Run a pod sandbox (using a config file) +```console +$ cat sandbox-config.json +{ + "metadata": { + "name": "nginx-sandbox", + "namespace": "default", + "attempt": 1, + "uid": "hdishd83djaidwnduwk28bcsb" + }, + "linux": { + } +} + +$ crictl runp sandbox-config.json +e1c83b0b8d481d4af8ba98d5f7812577fc175a37b10dc824335951f52addbb4e +$ crictl pods +PODSANDBOX ID CREATED STATE NAME NAMESPACE ATTEMPT +e1c83b0b8d481 2 hours ago SANDBOX_READY nginx-sandbox default 1 +$ crictl inspectp e1c8 + ... displays information about the pod and the pod sandbox pause container. +``` +* Note: As shown above, you may use truncated IDs if they are unique. +* Other commands to manage the pod include `stops ID` to stop a running pod and +`rmp ID` to remove a pod sandbox. + +## Create and Run a Container in the Pod Sandbox (using a config file) +```console +$ cat container-config.json +{ + "metadata": { + "name": "busybox" + }, + "image":{ + "image": "busybox" + }, + "command": [ + "top" + ], + "linux": { + } +} + +$ crictl create e1c83 container-config.json sandbox-config.json +0a2c761303163f2acaaeaee07d2ba143ee4cea7e3bde3d32190e2a36525c8a05 +$ crictl ps -a +CONTAINER ID IMAGE CREATED STATE NAME ATTEMPT +0a2c761303163 docker.io/busybox 2 hours ago CONTAINER_CREATED busybox 0 +$ crictl start 0a2c +0a2c761303163f2acaaeaee07d2ba143ee4cea7e3bde3d32190e2a36525c8a05 +$ crictl ps +CONTAINER ID IMAGE CREATED STATE NAME ATTEMPT +0a2c761303163 docker.io/busybox 2 hours ago CONTAINER_RUNNING busybox 0 +$ crictl inspect 0a2c7 + ... show detailed information about the container +``` +## Exec a Command in the Container +```console +$ crictl exec -i -t 0a2c ls +bin dev etc home proc root sys tmp usr var +``` +## Display Stats for the Container +```console +$ crictl stats +CONTAINER CPU % MEM DISK INODES +0a2c761303163f 0.00 983kB 16.38kB 6 +``` +* Other commands to manage the container include `stop ID` to stop a running +container and `rm ID` to remove a container. +## Display Version Information +```console +$ crictl version +Version: 0.1.0 +RuntimeName: containerd +RuntimeVersion: 1.0.0-beta.1-186-gdd47a72-TEST +RuntimeApiVersion: v1alpha2 +``` +## Display Status & Configuration Information about Containerd & The CRI Plugin +```console +$ crictl info +{ + "status": { + "conditions": [ + { + "type": "RuntimeReady", + "status": true, + "reason": "", + "message": "" + }, + { + "type": "NetworkReady", + "status": true, + "reason": "", + "message": "" + } + ] + }, + "config": { + "containerd": { + "snapshotter": "overlayfs", + "runtime": "io.containerd.runtime.v1.linux" + }, + "cni": { + "binDir": "/opt/cni/bin", + "confDir": "/etc/cni/net.d" + }, + "registry": { + "mirrors": { + "docker.io": { + "endpoint": [ + "https://registry-1.docker.io" + ] + } + } + }, + "streamServerPort": "10010", + "sandboxImage": "k8s.gcr.io/pause:3.2", + "statsCollectPeriod": 10, + "containerdRootDir": "/var/lib/containerd", + "containerdEndpoint": "unix:///run/containerd/containerd.sock", + "rootDir": "/var/lib/containerd/io.containerd.grpc.v1.cri", + "stateDir": "/run/containerd/io.containerd.grpc.v1.cri", + }, + "golang": "go1.10" +} +``` +## More Information +See [here](https://github.com/kubernetes-sigs/cri-tools/blob/master/docs/crictl.md) +for information about crictl. diff --git a/docs/decryption.md b/docs/decryption.md new file mode 100644 index 000000000..abde945b8 --- /dev/null +++ b/docs/decryption.md @@ -0,0 +1,46 @@ +# Configure Image Decryption +This document describes the method to configure encrypted container image decryption for `containerd` for use with the `cri` plugin. + +## Encrypted Container Images + +Encrypted container images are OCI images which contain encrypted blobs. These encrypted images can be created through the use of [containerd/imgcrypt project](https://github.com/containerd/imgcrypt). To decrypt these images, the `containerd` runtime uses information passed from the `cri` such as keys, options and encryption metadata. + +## The "node" Key Model + +Encryption ties trust to an entity based on the model in which a key is associated with it. We call this the key model. One such usecase is when we want to tie the trust of a key to the node in a cluster. In this case, we call it the "node" or "host" Key Model. Future work will include more key models to facilitate other trust associations (i.e. for multi-tenancy). + +### "node" Key Model Usecase + +In this model encryption is tied to worker nodes. The usecase here revolves around the idea that an image should be decryptable only on trusted host. Using this model, various node based technologies which help bootstrap trust in worker nodes and perform secure key distribution (i.e. TPM, host attestation, secure/measured boot). In this scenario, runtimes are capable of fetching the necessary decryption keys. An example of this is using the [`--decryption-keys-path` flag in imgcrypt](https://github.com/containerd/imgcrypt). + +### Configuring image decryption for "node" key model + +The default configuration does not handle decrypting encrypted container images. + +An example for configuring the "node" key model for container image decryption: + +Configure `cri` to enable decryption with "node" key model +```toml +[plugins."io.containerd.grpc.v1.cri".image_decryption] + key_model = "node" +``` + +Configure `containerd` daemon [`stream_processors`](https://github.com/containerd/containerd/blob/master/docs/stream_processors.md) to handle the +encrypted mediatypes. +```toml +[stream_processors] + [stream_processors."io.containerd.ocicrypt.decoder.v1.tar.gzip"] + accepts = ["application/vnd.oci.image.layer.v1.tar+gzip+encrypted"] + returns = "application/vnd.oci.image.layer.v1.tar+gzip" + path = "/usr/local/bin/ctd-decoder" + args = ["--decryption-keys-path", "/keys"] + [stream_processors."io.containerd.ocicrypt.decoder.v1.tar"] + accepts = ["application/vnd.oci.image.layer.v1.tar+encrypted"] + returns = "application/vnd.oci.image.layer.v1.tar" + path = "/usr/local/bin/ctd-decoder" + args = ["--decryption-keys-path", "/keys"] +``` + +In this example, container image decryption is set to use the "node" key model. In addition, the decryption [`stream_processors`](https://github.com/containerd/containerd/blob/master/docs/stream_processors.md) are configured as specified in [containerd/imgcrypt project](https://github.com/containerd/imgcrypt), with the additional field `--decryption-keys-path` configured to specify where decryption keys are located locally in the node. + +After modify this config, you need restart the `containerd` service. diff --git a/docs/installation.md b/docs/installation.md new file mode 100644 index 000000000..2337ffc6a --- /dev/null +++ b/docs/installation.md @@ -0,0 +1,108 @@ +# Install Containerd with Release Tarball +This document provides the steps to install `containerd` and its dependencies with the release tarball, and bring up a Kubernetes cluster using kubeadm. + +These steps have been verified on Ubuntu 16.04. For other OS distributions, the steps may differ. Please feel free to file issues or PRs if you encounter any problems on other OS distributions. + +*Note: You need to run the following steps on each node you are planning to use in your Kubernetes cluster.* +## Release Tarball +For each `containerd` release, we'll publish a release tarball specifically for Kubernetes named `cri-containerd-${VERSION}.${OS}-${ARCH}.tar.gz`. This release tarball contains all required binaries and files for using `containerd` with Kubernetes. For example, the 1.2.4 version is available at https://storage.googleapis.com/cri-containerd-release/cri-containerd-1.2.4.linux-amd64.tar.gz. + +Note: The VERSION tag specified for the tarball corresponds to the `containerd` release tag, not a containerd/cri repository release tag. The `containerd` release includes the containerd/cri repository code through vendoring. The containerd/cri version of the containerd/cri code included in `containerd` is specified via a commit hash for containerd/cri in containerd/containerd/vendor.conf. +### Content +As shown below, the release tarball contains: +1) `containerd`, `containerd-shim`, `containerd-stress`, `containerd-release`, `ctr`: binaries for containerd. +2) `runc`: runc binary. +3) `crictl`, `crictl.yaml`: command line tools for CRI container runtime and its config file. +4) `critest`: binary to run [CRI validation test](https://github.com/kubernetes-sigs/cri-tools/blob/master/docs/validation.md). +5) `containerd.service`: Systemd unit for containerd. +6) `/opt/containerd/cluster/`: scripts for `kube-up.sh`. +```console +$ tar -tf cri-containerd-1.1.0-rc.0.linux-amd64.tar.gz +./ +./opt +./opt/containerd +./opt/containerd/cluster +./opt/containerd/cluster/gce +./opt/containerd/cluster/gce/cloud-init +./opt/containerd/cluster/gce/cloud-init/node.yaml +./opt/containerd/cluster/gce/cloud-init/master.yaml +./opt/containerd/cluster/gce/configure.sh +./opt/containerd/cluster/gce/env +./opt/containerd/cluster/version +./opt/containerd/cluster/health-monitor.sh +./usr +./usr/local +./usr/local/sbin +./usr/local/sbin/runc +./usr/local/bin +./usr/local/bin/crictl +./usr/local/bin/containerd +./usr/local/bin/containerd-stress +./usr/local/bin/critest +./usr/local/bin/containerd-release +./usr/local/bin/containerd-shim +./usr/local/bin/ctr +./etc +./etc/systemd +./etc/systemd/system +./etc/systemd/system/containerd.service +./etc/crictl.yaml +``` +### Binary Information +Information about the binaries in the release tarball: + +| Binary Name | Support | OS | Architecture | +|:------------------------------:|:------------------:|:-----:|:------------:| +| containerd | seccomp, apparmor,
overlay, btrfs | linux | amd64 | +| containerd-shim | overlay, btrfs | linux | amd64 | +| runc | seccomp, apparmor | linux | amd64 | + + +If you have other requirements for the binaries, e.g. selinux support, another architecture support etc., you need to build the binaries yourself following [the instructions](../README.md#getting-started-for-developers). + +### Download + +The release tarball could be downloaded from the release GCS bucket https://storage.googleapis.com/cri-containerd-release/. + +## Step 0: Install Dependent Libraries +Install required library for seccomp. +```bash +sudo apt-get update +sudo apt-get install libseccomp2 +``` +Note that: +1) If you are using Ubuntu <=Trusty or Debian <=jessie, a backported version of `libseccomp2` is needed. (See the [trusty-backports](https://packages.ubuntu.com/trusty-backports/libseccomp2) and [jessie-backports](https://packages.debian.org/jessie-backports/libseccomp2)). +## Step 1: Download Release Tarball +Download release tarball for the `containerd` version you want to install from the GCS bucket. +```bash +wget https://storage.googleapis.com/cri-containerd-release/cri-containerd-${VERSION}.linux-amd64.tar.gz +``` +Validate checksum of the release tarball: +```bash +sha256sum cri-containerd-${VERSION}.linux-amd64.tar.gz +curl https://storage.googleapis.com/cri-containerd-release/cri-containerd-${VERSION}.linux-amd64.tar.gz.sha256 +# Compare to make sure the 2 checksums are the same. +``` +## Step 2: Install Containerd +If you are using systemd, just simply unpack the tarball to the root directory: +```bash +sudo tar --no-overwrite-dir -C / -xzf cri-containerd-${VERSION}.linux-amd64.tar.gz +sudo systemctl start containerd +``` +If you are not using systemd, please unpack all binaries into a directory in your `PATH`, and start `containerd` as monitored long running services with the service manager you are using e.g. `supervisord`, `upstart` etc. +## Step 3: Install Kubeadm, Kubelet and Kubectl +Follow [the instructions](https://kubernetes.io/docs/setup/independent/install-kubeadm/) to install kubeadm, kubelet and kubectl. +## Step 4: Create Systemd Drop-In for Containerd +Create the systemd drop-in file `/etc/systemd/system/kubelet.service.d/0-containerd.conf`: +``` +[Service] +Environment="KUBELET_EXTRA_ARGS=--container-runtime=remote --runtime-request-timeout=15m --container-runtime-endpoint=unix:///run/containerd/containerd.sock" +``` +And reload systemd configuration: +```bash +systemctl daemon-reload +``` +## Bring Up the Cluster +Now you should have properly installed all required binaries and dependencies on each of your node. + +The next step is to use kubeadm to bring up the Kubernetes cluster. It is the same with [the ansible installer](../contrib/ansible). Please follow the steps 2-4 [here](../contrib/ansible/README.md#step-2). diff --git a/docs/kube-up.md b/docs/kube-up.md new file mode 100644 index 000000000..31be73461 --- /dev/null +++ b/docs/kube-up.md @@ -0,0 +1,25 @@ +# Production Quality Cluster on GCE +This document provides the steps to bring up a production quality cluster on GCE with [`kube-up.sh`](https://kubernetes.io/docs/setup/turnkey/gce/). + +**If your Kubernetes version is 1.15 or greater, you can simply run:** +``` +export KUBE_CONTAINER_RUNTIME=containerd +``` +Follow these instructions [here](https://kubernetes.io/docs/setup/turnkey/gce/) to create a production quality Kubernetes cluster on GCE. +## Download CRI-Containerd Release Tarball +To download release tarball, see [step 1](./installation.md#step-1-download-cri-containerd-release-tarball) in installation.md. + +Unpack release tarball to any directory, using `${CRI_CONTAINERD_PATH}` to indicate the directory in the doc: +```bash +tar -C ${CRI_CONTAINERD_PATH} -xzf cri-containerd-${VERSION}.linux-amd64.tar.gz +``` +## Set Environment Variables for CRI-Containerd +```bash +. ${CRI_CONTAINERD_PATH}/opt/containerd/cluster/gce/env +``` +## Create Kubernetes Cluster on GCE +Follow these instructions [here](https://kubernetes.io/docs/setup/turnkey/gce/) to create a production quality Kubernetes cluster on GCE. + +**Make sure the Kubernetes version you are using is v1.11 or greater:** +* When using `https://get.k8s.io`, use the environment variable `KUBERNETES_RELEASE` to set version. +* When using a Kubernetes release tarball, make sure to select version 1.11 or greater. diff --git a/docs/performance.png b/docs/performance.png new file mode 100644 index 000000000..387fa9bb3 Binary files /dev/null and b/docs/performance.png differ diff --git a/docs/proposal.md b/docs/proposal.md new file mode 100644 index 000000000..64db560db --- /dev/null +++ b/docs/proposal.md @@ -0,0 +1,111 @@ +Containerd CRI Integration +============= +Author: Lantao Liu (@random-liu) +## Abstract +This proposal aims to integrate [containerd](https://github.com/containerd/containerd) with Kubelet against the [container runtime interface (CRI)](https://github.com/kubernetes/kubernetes/blob/v1.6.0/pkg/kubelet/api/v1alpha1/runtime/api.proto). +## Background +Containerd is a core container runtime, which provides the minimum set of functionalities to manage the complete container lifecycle of its host system, including container execution and supervision, image distribution and storage, etc. + +Containerd was [introduced in Docker 1.11](https://blog.docker.com/2016/04/docker-engine-1-11-runc/), used to manage [runC](https://runc.io/) containers on the node. As shown below, it creates a containerd-shim for each container, and the shim manages the lifecycle of its corresponding container. +![containerd](./containerd.png) + +In Dec. 2016, Docker Inc. spun it out into a standalone component, and donated it to [CNCF](https://www.cncf.io/) in Mar. 2017. + +## Motivation +Containerd is one potential alternative to Docker as the runtime for Kubernetes clusters. *Compared with Docker*, containerd has pros and cons. +### Pros +* **Stability**: Containerd has limited scope and slower feature velocity, which is expected to be more stable. +* **Compatibility**: The scope of containerd aligns with Kubernetes' requirements. It provides the required functionalities and the flexibility for areas like image pulling, networking, volume and logging etc. +* **Performance**: + * Containerd consumes less resource than Docker at least because it's a subset of Docker; + * Containerd CRI integration eliminates an extra hop in the stack (as shown below). ![performance](./performance.png) +* **Neutral Foundation**: Containerd is part of CNCF now. +### Cons +* **User Adoption**: + * Ideally, Kubernetes users don't interact with the underlying container runtime directly. However, for the lack of debug toolkits, sometimes users still need to login the node to debug with Docker CLI directly. + * Containerd provides barebone CLIs [ctr](https://github.com/containerd/containerd/tree/master/cmd/ctr) and [dist](https://github.com/containerd/containerd/tree/master/cmd/dist) for development and debugging purpose, but they may not be sufficient and necessary. Additionally, presuming these are sufficient and necessary tools, a plan and time would be needed to sufficiently document these CLIs and educate users in their use. +* **Maturity**: The rescoped containerd is pretty new, and it's still under heavy development. +## Goals +* Make sure containerd meets the requirement of Kubernetes, now and into the foreseeable future. +* Implement containerd CRI shim and make sure it provides equivalent functionality, usability and debuggability. +* Improve Kubernetes by taking advantage of the flexibility provided by containerd. +## Design +The following sections discuss the design aspects of the containerd CRI integration. For the purposes of this doc, the containerd CRI integration will be referred to as `CRI-containerd`. +### Container Lifecycle +CRI-containerd relies on containerd to manage container lifecycle. + +Ideally, CRI-containerd only needs to do api translation and information reorganization. However, CRI-containerd needs to maintain some metadata because: +* There is a mismatch between container lifecycle of CRI and containerd - containerd only tracks running processes, once the container and it's corresponding containerd-shim exit, the container is no longer visible in the containerd API. +* Some sandbox/container metadata is not provided by containerd, and we can not leverage OCI runtime annotation to store it because of the container lifecycle mismatch, e.g. labels/annotations, `PodSandboxID` of a container, `FinishedAt` timestamp, `ExitCode`, `Mounts` etc. + +CRI-containerd should checkpoint these metadata itself or use [containerd metadata service](https://github.com/containerd/containerd/blob/0a5544d8c4dab44dfc682f5ad07f1cd011c0a115/design/plugins.md#core) if available. +### Container Logging +Containerd doesn't provide persistent container log. It redirects container STDIO into different FIFOs. + +CRI-containerd should start a goroutine (process/container in the future) to: +* Continuously drain the FIFO; +* Decorate the log line into [CRI-defined format](https://github.com/kubernetes/community/blob/master/contributors/design-proposals/node/kubelet-cri-logging.md#proposed-solution); +* Write the log into [CRI-defined log path](https://github.com/kubernetes/community/blob/master/contributors/design-proposals/node/kubelet-cri-logging.md#proposed-solution). +### Container Streaming +Containerd supports creating a process in the container with `Exec`, and the STDIO is also exposed as FIFOs. Containerd also supports resizing console of a specific process with `Pty`. + +CRI-containerd could reuse the [streaming server](https://github.com/kubernetes/kubernetes/blob/release-1.6/pkg/kubelet/server/streaming/server.go), it should implement the [streaming runtime interface](https://github.com/kubernetes/kubernetes/blob/release-1.6/pkg/kubelet/server/streaming/server.go#L61-L65). + +For different CRI streaming functions: +* `ExecSync`: CRI-containerd should use `Exec` to create the exec process, collect the stdout/stderr of the process, and wait for the process to terminate. +* `Exec`: CRI-containerd should use `Exec` to create the exec process, create a goroutine (process/container) to redirect streams, and wait for the process to terminate. +* `Attach`: CRI-containerd should create a goroutine (process/container) to read the existing container log to the output, redirect streams of the init process, and wait for any stream to be closed. +* `PortForward`: CRI-containerd could implement this with `socat` and `nsenter`, similar with [current Docker portforward implementation](https://github.com/kubernetes/kubernetes/blob/release-1.6/pkg/kubelet/dockertools/docker_manager.go#L1373-L1428). +### Container Networking +Containerd doesn't provide container networking, but OCI runtime spec supports joining a linux container into an existing network namespace. + +CRI-containerd should: +* Create a network namespace for a sandbox; +* Call [network plugin](https://github.com/kubernetes/kubernetes/blob/master/pkg/kubelet/network/plugins.go) to update the options of the network namespace; +* Let the user containers in the same sandbox share the network namespace. +### Container Metrics +Containerd provides [container cgroup metrics](https://github.com/containerd/containerd/blob/master/reports/2017-03-17.md#metrics), and plans to provide [container writable layer disk usage](https://github.com/containerd/containerd/issues/678). + +CRI container metrics api needs to be defined ([#27097](https://github.com/kubernetes/kubernetes/issues/27097)). After that, CRI-containerd should translate containerd container metrics into CRI container metrics. +### Image Management +CRI-containerd relies on containerd to manage images. Containerd should provide all function and information required by CRI, and CRI-containerd only needs to do api translation and information reorganization. + +### ImageFS Metrics +Containerd plans to provide [image filesystem metrics](https://github.com/containerd/containerd/issues/678). + +CRI image filesystem metrics needs to be defined ([#33048](https://github.com/kubernetes/kubernetes/issues/33048)). After that, we should make sure containerd provides the required metrics, and CRI-containerd should translate containerd image filesystem metrics into CRI image filesystem metrics. +### Out of Scope +Following items are out of the scope of this design, we may address them in future version as enhancement or optimization. +* **Debuggability**: One of the biggest concern of CRI-containerd is debuggability. We should provide equivalent debuggability with Docker CLI through `kubectl`, [`cri-tools`](https://github.com/kubernetes-sigs/cri-tools) or containerd CLI. +* **Built-in CRI support**: The [plugin model](https://github.com/containerd/containerd/blob/master/design/plugins.md) provided by containerd makes it possible to directly build CRI support into containerd as a plugin, which will eliminate one more hop from the stack. But because of the [limitation of golang plugin](https://github.com/containerd/containerd/issues/563), we have to either maintain our own branch or push CRI plugin upstream. +* **Seccomp**: ([#36997](https://github.com/kubernetes/kubernetes/issues/36997)) Seccomp is supported in OCI runtime spec. However, current seccomp implementation in Kubernetes is experimental and docker specific, the api needs to be defined in CRI first before CRI-containerd implements it. +* **Streaming server authentication**: ([#36666](https://github.com/kubernetes/kubernetes/issues/36666)) CRI-containerd will be out-of-process with Kubelet, so it could not reuse Kubelet authentication. Its streaming server should implement its own authentication mechanism. +* **Move container facilities into pod cgroup**: Container facilities including container image puller, container streaming handler, log handler and containerd-shim serve a specific container. They should be moved to the corresponding pod cgroup, and the overhead introduced by them should be charged to the pod. +* **Log rotation**: ([#42718](https://github.com/kubernetes/kubernetes/issues/42718)) Container log rotation is under design. A function may be added in CRI to signal the runtime to reopen log file. CRI-containerd should implement that function after it is defined. +* **Exec container**: With the flexibility provided by containerd, it is possible to implement `Exec` with a separate container sharing the same rootfs and mount namespace with the original container. The advantage is that the `Exec` container could have it's own sub-cgroup, so that it will not consume the resource of application container and user could specify dedicated resource for it. +* **Advanced image management**: The image management interface in CRI is relatively simple because the requirement of Kubelet image management is not clearly scoped out. In the future, we may want to leverage the flexibility provided by containerd more, e.g. estimate image size before pulling etc. +* ... +## Roadmap and Milestones +### Milestones +#### Kubernetes 1.7 - Q2 +* [P0] Basic container lifecycle. +* [P0] Basic image management. +* [P0] Container networking. +* [P1] Container streaming/logging. +* [P2] Container/ImageFS Metrics. + +*Test Plan: Each feature added should have unit test and pass its corresponding cri validation test.* +#### Kubernetes 1.8 - Q3 +* [P0] Feature complete, pass 100% cri validation test. +* [P0] Integrate CRI-containerd with Kubernetes, and build the e2e/node e2e test framework. +* [P1] Address the debuggability problem. +### Q2 Roadmap +| Item | 1/2 Mar. | 2/2 Mar. | 1/2 Apr. | 2/2 Apr. | 1/2 May. | 2/2 May. | +|:--------------------------------:|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:| +| Survey | ✓ | | | | | | +| POC | | ✓ | | | | | +| Proposal | | | ✓ | | | | +| Containerd Feature Complete | ✓ | ✓ | ✓ | | | | +| Runtime Management Integration | | | ✓ | ✓ | ✓ | ✓ | +| Image Management Integration | | | | ✓ | ✓ | ✓ | +| Container Networking Integration | | | | | ✓ | ✓ | diff --git a/docs/registry.md b/docs/registry.md new file mode 100644 index 000000000..2bf1a6633 --- /dev/null +++ b/docs/registry.md @@ -0,0 +1,187 @@ +# Configure Image Registry + +This document describes the method to configure the image registry for `containerd` for use with the `cri` plugin. + +NOTE: The configuration syntax used in this doc is in version 2 which is the +recommended since `containerd` 1.3. If your configuration is still in version 1, +you can replace `"io.containerd.grpc.v1.cri"` with `cri`. + +## Configure Registry Endpoint + +With containerd, `docker.io` is the default image registry. You can also set up other image registries similar to docker. + +To configure image registries create/modify the `/etc/containerd/config.toml` as follows: + +```toml +# Config file is parsed as version 1 by default. +# To use the long form of plugin names set "version = 2" +# explicitly use v2 config format +version = 2 + +[plugin."io.containerd.grpc.v1.cri".registry.mirrors] + [plugin."io.containerd.grpc.v1.cri".registry.mirrors."docker.io"] + endpoint = ["https://registry-1.docker.io"] + [plugin."io.containerd.grpc.v1.cri".registry.mirrors."test.https-registry.io"] + endpoint = ["https://HostIP1:Port1"] + [plugin."io.containerd.grpc.v1.cri".registry.mirrors."test.http-registry.io"] + endpoint = ["http://HostIP2:Port2"] + # wildcard matching is supported but not required. + [plugin."io.containerd.grpc.v1.cri".registry.mirrors."*"] + endpoint = ["https://HostIP3:Port3"] +``` + +The default configuration can be generated by `containerd config default > /etc/containerd/config.toml`. + +The endpoint is a list that can contain multiple image registry URLs split by commas. When pulling an image +from a registry, containerd will try these endpoint URLs one by one, and use the first working one. Please note +that if the default registry endpoint is not already specified in the endpoint list, it will be automatically +tried at the end with scheme `https` and path `v2`, e.g. `https://gcr.io/v2` for `gcr.io`. + +As an example, for the image `gcr.io/library/busybox:latest`, the endpoints are: + +* `gcr.io` is configured: endpoints for `gcr.io` + default endpoint `https://gcr.io/v2`. +* `*` is configured, and `gcr.io` is not: endpoints for `*` + default + endpoint `https://gcr.io/v2`. +* None of above is configured: default endpoint `https://gcr.io/v2`. + +After modify this config, you need restart the `containerd` service. + +## Configure Registry TLS Communication + +`cri` plugin also supports configuring TLS settings when communicating with a registry. + +To configure the TLS settings for a specific registry, create/modify the `/etc/containerd/config.toml` as follows: + +```toml +# explicitly use v2 config format +version = 2 + +# The registry host has to be a domain name or IP. Port number is also +# needed if the default HTTPS or HTTP port is not used. +[plugin."io.containerd.grpc.v1.cri".registry.configs."my.custom.registry".tls] + ca_file = "ca.pem" + cert_file = "cert.pem" + key_file = "key.pem" +``` + +In the config example shown above, TLS mutual authentication will be used for communications with the registry endpoint located at . +`ca_file` is file name of the certificate authority (CA) certificate used to authenticate the x509 certificate/key pair specified by the files respectively pointed to by `cert_file` and `key_file`. + +`cert_file` and `key_file` are not needed when TLS mutual authentication is unused. + +```toml +# explicitly use v2 config format +version = 2 + +[plugin."io.containerd.grpc.v1.cri".registry.configs."my.custom.registry".tls] + ca_file = "ca.pem" +``` + +To skip the registry certificate verification: + +```toml +# explicitly use v2 config format +version = 2 + +[plugin."io.containerd.grpc.v1.cri".registry.configs."my.custom.registry".tls] + insecure_skip_verify = true +``` + +## Configure Registry Credentials + +`cri` plugin also supports docker like registry credential config. + +To configure a credential for a specific registry, create/modify the +`/etc/containerd/config.toml` as follows: + +```toml +# explicitly use v2 config format +version = 2 + +# The registry host has to be a domain name or IP. Port number is also +# needed if the default HTTPS or HTTP port is not used. +[plugin."io.containerd.grpc.v1.cri".registry.configs."gcr.io".auth] + username = "" + password = "" + auth = "" + identitytoken = "" +``` + +The meaning of each field is the same with the corresponding field in `.docker/config.json`. + +Please note that auth config passed by CRI takes precedence over this config. +The registry credential in this config will only be used when auth config is +not specified by Kubernetes via CRI. + +After modifying this config, you need to restart the `containerd` service. + +### Configure Registry Credentials Example - GCR with Service Account Key Authentication + +If you don't already have Google Container Registry (GCR) set-up then you need to do the following steps: + +* Create a Google Cloud Platform (GCP) account and project if not already created (see [GCP getting started](https://cloud.google.com/gcp/getting-started)) +* Enable GCR for your project (see [Quickstart for Container Registry](https://cloud.google.com/container-registry/docs/quickstart)) +* For authentication to GCR: Create [service account and JSON key](https://cloud.google.com/container-registry/docs/advanced-authentication#json-key) +* The JSON key file needs to be downloaded to your system from the GCP console +* For access to the GCR storage: Add service account to the GCR storage bucket with storage admin access rights (see [Granting permissions](https://cloud.google.com/container-registry/docs/access-control#grant-bucket)) + +Refer to [Pushing and pulling images](https://cloud.google.com/container-registry/docs/pushing-and-pulling) for detailed information on the above steps. + +> Note: The JSON key file is a multi-line file and it can be cumbersome to use the contents as a key outside of the file. It is worthwhile generating a single line format output of the file. One way of doing this is using the `jq` tool as follows: `jq -c . key.json` + +It is beneficial to first confirm that from your terminal you can authenticate with your GCR and have access to the storage before hooking it into containerd. This can be verified by performing a login to your GCR and +pushing an image to it as follows: + +```console +docker login -u _json_key -p "$(cat key.json)" gcr.io + +docker pull busybox + +docker tag busybox gcr.io/your-gcp-project-id/busybox + +docker push gcr.io/your-gcp-project-id/busybox + +docker logout gcr.io +``` + +Now that you know you can access your GCR from your terminal, it is now time to try out containerd. + +Edit the containerd config (default location is at `/etc/containerd/config.toml`) +to add your JSON key for `gcr.io` domain image pull +requests: + +```toml +version = 2 + +[plugins."io.containerd.grpc.v1.cri".registry] + [plugins."io.containerd.grpc.v1.cri".registry.mirrors] + [plugins."io.containerd.grpc.v1.cri".registry.mirrors."docker.io"] + endpoint = ["https://registry-1.docker.io"] + [plugins."io.containerd.grpc.v1.cri".registry.mirrors."gcr.io"] + endpoint = ["https://gcr.io"] + [plugins."io.containerd.grpc.v1.cri".registry.configs] + [plugins."io.containerd.grpc.v1.cri".registry.configs."gcr.io".auth] + username = "_json_key" + password = 'paste output from jq' +``` + +> Note: `username` of `_json_key` signifies that JSON key authentication will be used. + +Restart containerd: + +```console +service containerd restart +``` + +Pull an image from your GCR with `crictl`: + +```console +$ sudo crictl pull gcr.io/your-gcp-project-id/busybox + +DEBU[0000] get image connection +DEBU[0000] connect using endpoint 'unix:///run/containerd/containerd.sock' with '3s' timeout +DEBU[0000] connected successfully using endpoint: unix:///run/containerd/containerd.sock +DEBU[0000] PullImageRequest: &PullImageRequest{Image:&ImageSpec{Image:gcr.io/your-gcr-instance-id/busybox,},Auth:nil,SandboxConfig:nil,} +DEBU[0001] PullImageResponse: &PullImageResponse{ImageRef:sha256:78096d0a54788961ca68393e5f8038704b97d8af374249dc5c8faec1b8045e42,} +Image is up to date for sha256:78096d0a54788961ca68393e5f8038704b97d8af374249dc5c8faec1b8045e42 +``` diff --git a/docs/release.md b/docs/release.md new file mode 100644 index 000000000..1fc32264c --- /dev/null +++ b/docs/release.md @@ -0,0 +1,27 @@ +# Release Process +This document describes how to cut a `cri` plugin release. + +## Step 1: Update containerd vendor +Update the version of containerd located in `containerd/cri/vendor.conf` +to the latest version of containerd for the desired branch of containerd, +and make sure all tests in CI pass https://k8s-testgrid.appspot.com/sig-node-containerd. +## Step 2: Cut the release +Draft and tag a new release in https://github.com/containerd/cri/releases. +## Step 3: Update `cri` version in containerd +Push a PR to `containerd/containerd` that updates the version of +`containerd/cri` in `containerd/containerd/vendor.conf` to the newly +tagged release created in Step 2. +## Step 4: Iterate step 1 updating containerd vendor +## Step 5: Publish release tarball for Kubernetes +Publish the release tarball `cri-containerd-${CONTAINERD_VERSION}.${OS}-${ARCH}.tar.gz` +```shell +# Checkout `containerd/cri` to the newly released version. +git checkout ${RELEASE_VERSION} + +# Publish the release tarball without cni. +DEPLOY_BUCKET=cri-containerd-release make push TARBALL_PREFIX=cri-containerd OFFICIAL_RELEASE=true VERSION=${CONTAINERD_VERSION} + +# Publish the release tarball with cni. +DEPLOY_BUCKET=cri-containerd-release make push TARBALL_PREFIX=cri-containerd-cni OFFICIAL_RELEASE=true INCLUDE_CNI=true VERSION=${CONTAINERD_VERSION} +``` +## Step 6: Update release note with release tarball information diff --git a/docs/testing.md b/docs/testing.md new file mode 100644 index 000000000..ba74db5d2 --- /dev/null +++ b/docs/testing.md @@ -0,0 +1,58 @@ +CRI Plugin Testing Guide +======================== +This document assumes you have already setup the development environment (go, git, `containerd/cri` repo etc.). + +Before sending pull requests you should at least make sure your changes have passed code verification, unit, integration and CRI validation tests. +## Code Verification +Code verification includes lint, and code formatting check etc. +* Install tools used by code verification: +```bash +make install.tools +``` +***Note:*** Some make actions (like `install.tools`) use the user's `GOPATH` and will otherwise not work when it is not set. Other make actions override it by setting it to a temporary directory for release build and testing purposes. +* Run code verification: +```bash +make verify +``` +## Unit Test +Run all unit tests in `containerd/cri` repo. +```bash +make test +``` +## Integration Test +Run all integration tests in `containerd/cri` repo. +* [Install dependencies](../README.md#install-dependencies). +* Run integration test: +```bash +make test-integration +``` +## CRI Validation Test +[CRI validation test](https://github.com/kubernetes/community/blob/master/contributors/devel/cri-validation.md) is a test framework for validating that a Container Runtime Interface (CRI) implementation such as containerd with the `cri` plugin meets all the requirements necessary to manage pod sandboxes, containers, images etc. + +CRI validation test makes it possible to verify CRI conformance of `containerd/cri` without setting up Kubernetes components or running Kubernetes end-to-end tests. +* [Install dependencies](../README.md#install-dependencies). +* Build containerd with the `cri` plugin: +```bash +make +``` +* Run CRI validation test: +```bash +make test-cri +``` +* Focus or skip specific CRI validation test: +```bash +make test-cri FOCUS=REGEXP_TO_FOCUS SKIP=REGEXP_TO_SKIP +``` +[More information](https://github.com/kubernetes-sigs/cri-tools) about CRI validation test. +## Node E2E Test +[Node e2e test](https://github.com/kubernetes/community/blob/master/contributors/devel/e2e-node-tests.md) is a test framework testing Kubernetes node level functionalities such as managing pods, mounting volumes etc. It starts a local cluster with Kubelet and a few other minimum dependencies, and runs node functionality tests against the local cluster. +* [Install dependencies](../README.md#install-dependencies). +* Run node e2e test: +```bash +make test-e2e-node +``` +* Focus or skip specific node e2e test: +```bash +make test-e2e-node FOCUS=REGEXP_TO_FOCUS SKIP=REGEXP_TO_SKIP +``` +[More information](https://github.com/kubernetes/community/blob/master/contributors/devel/e2e-node-tests.md) about Kubernetes node e2e test. diff --git a/hack/boilerplate/boilerplate b/hack/boilerplate/boilerplate new file mode 100644 index 000000000..c073fa4ad --- /dev/null +++ b/hack/boilerplate/boilerplate @@ -0,0 +1,15 @@ +/* + Copyright The containerd 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 + + http://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. +*/ diff --git a/hack/install/install-cni-config.sh b/hack/install/install-cni-config.sh new file mode 100755 index 000000000..93f574408 --- /dev/null +++ b/hack/install/install-cni-config.sh @@ -0,0 +1,57 @@ +#!/bin/bash + +# Copyright The containerd 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 + +# http://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. + +set -o errexit +set -o nounset +set -o pipefail + +source $(dirname "${BASH_SOURCE[0]}")/utils.sh +CNI_CONFIG_DIR=${DESTDIR}/etc/cni/net.d +${SUDO} mkdir -p ${CNI_CONFIG_DIR} +${SUDO} bash -c 'cat >'${CNI_CONFIG_DIR}'/10-containerd-net.conflist <"'"${CRICTL_CONFIG_DIR}"'"/crictl.yaml <= 8 )); then + mask=255 + elif (( len > 0 )); then + mask=$(( 256 - 2 ** ( 8 - len ) )) + else + mask=0 + fi + (( len -= 8 )) + result_array[i]=$(( gateway_array[i] & mask )) + done + result="$(printf ".%s" "${result_array[@]}")" + result="${result:1}" + echo "$result/$((32 - prefix_len))" +} + +# nat already exists on the Windows VM, the subnet and gateway +# we specify should match that. +gateway="$(powershell -c "(Get-NetIPAddress -InterfaceAlias 'vEthernet (nat)' -AddressFamily IPv4).IPAddress")" +prefix_len="$(powershell -c "(Get-NetIPAddress -InterfaceAlias 'vEthernet (nat)' -AddressFamily IPv4).PrefixLength")" + +subnet="$(calculate_subnet "$gateway" "$prefix_len")" + +# The "name" field in the config is used as the underlying +# network type right now (see +# https://github.com/microsoft/windows-container-networking/pull/45), +# so it must match a network type in: +# https://docs.microsoft.com/en-us/windows-server/networking/technologies/hcn/hcn-json-document-schemas +bash -c 'cat >"'"${CNI_CONFIG_DIR}"'"/0-containerd-nat.conf < /dev/null; then + create_ttl_bucket ${DEPLOY_BUCKET} +fi + +if [ -z "${DEPLOY_DIR}" ]; then + DEPLOY_PATH="${DEPLOY_BUCKET}" +else + DEPLOY_PATH="${DEPLOY_BUCKET}/${DEPLOY_DIR}" +fi + +# TODO(random-liu): Add checksum for the tarball. +gsutil cp ${release_tar} "gs://${DEPLOY_PATH}/" +gsutil cp ${release_tar_checksum} "gs://${DEPLOY_PATH}/" +echo "Release tarball is uploaded to: + https://storage.googleapis.com/${DEPLOY_PATH}/${TARBALL}" + +if ${PUSH_VERSION}; then + if [[ -z "${VERSION}" ]]; then + echo "VERSION is not set" + exit 1 + fi + echo ${VERSION} | gsutil cp - "gs://${DEPLOY_PATH}/${LATEST}" + echo "Latest version is uploaded to: + https://storage.googleapis.com/${DEPLOY_PATH}/${LATEST}" +fi diff --git a/hack/release-windows.sh b/hack/release-windows.sh new file mode 100755 index 000000000..da01f5384 --- /dev/null +++ b/hack/release-windows.sh @@ -0,0 +1,75 @@ +#!/bin/bash + +# Copyright The containerd 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 + +# http://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. + +set -o errexit +set -o nounset +set -o pipefail + +source $(dirname "${BASH_SOURCE[0]}")/utils.sh +cd ${ROOT} + +umask 0022 + +# BUILD_DIR is the directory to generate release tar. +# TARBALL is the name of the release tar. +BUILD_DIR=${BUILD_DIR:-"_output"} +# Convert to absolute path if it's relative. +if [[ ${BUILD_DIR} != /* ]]; then + BUILD_DIR=${ROOT}/${BUILD_DIR} +fi +TARBALL=${TARBALL:-"cri-containerd.tar.gz"} +# INCLUDE_CNI indicates whether to install CNI. By default don't +# include CNI in release tarball. +INCLUDE_CNI=${INCLUDE_CNI:-false} +# CUSTOM_CONTAINERD indicates whether to install customized containerd +# for CI test. +CUSTOM_CONTAINERD=${CUSTOM_CONTAINERD:-false} + +destdir=${BUILD_DIR}/release-stage + +if [[ -z "${VERSION}" ]]; then + echo "VERSION is not set" + exit 1 +fi + +# Remove release-stage directory to avoid including old files. +rm -rf ${destdir} + +# Install dependencies into release stage. +# Install hcsshim +HCSSHIM_DIR=${destdir} ./hack/install/windows/install-hcsshim.sh + +if ${INCLUDE_CNI}; then + # Install cni + NOSUDO=true WINCNI_BIN_DIR=${destdir}/cni ./hack/install/windows/install-cni.sh +fi + +# Build containerd from source +NOSUDO=true CONTAINERD_DIR=${destdir} ./hack/install/install-containerd.sh +# Containerd makefile always installs into a "bin" directory. +mv "${destdir}"/bin/* "${destdir}" +rm -rf "${destdir}/bin" + +if ${CUSTOM_CONTAINERD}; then + make install -e BINDIR=${destdir} +fi + +# Create release tar +tarball=${BUILD_DIR}/${TARBALL} +tar -zcvf ${tarball} -C ${destdir} . --owner=0 --group=0 +checksum=$(sha256 ${tarball}) +echo "sha256sum: ${checksum} ${tarball}" +echo ${checksum} > ${tarball}.sha256 diff --git a/hack/release.sh b/hack/release.sh new file mode 100755 index 000000000..a70c53268 --- /dev/null +++ b/hack/release.sh @@ -0,0 +1,127 @@ +#!/bin/bash + +# Copyright The containerd 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 + +# http://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. + +set -o errexit +set -o nounset +set -o pipefail + +source $(dirname "${BASH_SOURCE[0]}")/utils.sh +cd ${ROOT} + +umask 0022 + +# BUILD_DIR is the directory to generate release tar. +# TARBALL is the name of the release tar. +BUILD_DIR=${BUILD_DIR:-"_output"} +# Convert to absolute path if it's relative. +if [[ ${BUILD_DIR} != /* ]]; then + BUILD_DIR=${ROOT}/${BUILD_DIR} +fi +TARBALL=${TARBALL:-"cri-containerd.tar.gz"} +# INCLUDE_CNI indicates whether to install CNI. By default don't +# include CNI in release tarball. +INCLUDE_CNI=${INCLUDE_CNI:-false} +# CUSTOM_CONTAINERD indicates whether to install customized containerd +# for CI test. +CUSTOM_CONTAINERD=${CUSTOM_CONTAINERD:-false} +# OFFICIAL_RELEASE indicates whether to use official containerd release. +OFFICIAL_RELEASE=${OFFICIAL_RELEASE:-false} +# LOCAL_RELEASE indicates that containerd has been built and released +# locally. +LOCAL_RELEASE=${LOCAL_RELEASE:-false} +if [ -z "${GOOS:-}" ] +then + GOOS=$(go env GOOS) +fi +if [ -z "${GOARCH:-}" ] +then + GOARCH=$(go env GOARCH) +fi + + +destdir=${BUILD_DIR}/release-stage + +if [[ -z "${VERSION}" ]]; then + echo "VERSION is not set" + exit 1 +fi + +# Remove release-stage directory to avoid including old files. +rm -rf ${destdir} + +# download_containerd downloads containerd from official release. +download_containerd() { + local -r tmppath="$(mktemp -d /tmp/download-containerd.XXXX)" + local -r tarball="${tmppath}/containerd.tar.gz" + local -r url="https://github.com/containerd/containerd/releases/download/v${VERSION}/containerd-${VERSION}.linux-amd64.tar.gz" + wget -O "${tarball}" "${url}" + tar -C "${destdir}/usr/local" -xzf "${tarball}" + rm -rf "${tmppath}" +} + +# copy_local_containerd copies local containerd release. +copy_local_containerd() { + local -r tarball="${GOPATH}/src/github.com/containerd/containerd/releases/containerd-${VERSION}.${GOOS}-${GOARCH}.tar.gz" + if [[ ! -e "${tarball}" ]]; then + echo "Containerd release is not built" + exit 1 + fi + tar -C "${destdir}/usr/local" -xzf "${tarball}" +} + +# Install dependencies into release stage. +# Install runc +NOSUDO=true DESTDIR=${destdir} ./hack/install/install-runc.sh + +if ${INCLUDE_CNI}; then + # Install cni + NOSUDO=true DESTDIR=${destdir} ./hack/install/install-cni.sh +fi + +# Install critools +NOSUDO=true DESTDIR=${destdir} ./hack/install/install-critools.sh + +# Install containerd +if $OFFICIAL_RELEASE; then + download_containerd +elif $LOCAL_RELEASE; then + copy_local_containerd +else + # Build containerd from source + NOSUDO=true DESTDIR=${destdir} ./hack/install/install-containerd.sh +fi + +if ${CUSTOM_CONTAINERD}; then + make install -e DESTDIR=${destdir} +fi + +# Install systemd units into release stage. +mkdir -p ${destdir}/etc/systemd/system +cp ${ROOT}/contrib/systemd-units/* ${destdir}/etc/systemd/system/ +# Install cluster directory into release stage. +mkdir -p ${destdir}/opt/containerd +cp -r ${ROOT}/cluster ${destdir}/opt/containerd +# Write a version file into the release tarball. +cat > ${destdir}/opt/containerd/cluster/version < ${tarball}.sha256 diff --git a/hack/sort-vendor.sh b/hack/sort-vendor.sh new file mode 100755 index 000000000..988d1560d --- /dev/null +++ b/hack/sort-vendor.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +# Copyright The containerd 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 + +# http://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. + +set -o errexit +set -o nounset +set -o pipefail + +source $(dirname "${BASH_SOURCE[0]}")/utils.sh +cd ${ROOT} + +echo "Sort vendor.conf..." +tmpdir="$(mktemp -d)" +trap "rm -rf ${tmpdir}" EXIT + +awk -v RS= '{print > "'${tmpdir}/'TMP."NR}' vendor.conf +for file in ${tmpdir}/*; do + if [[ -e "${tmpdir}/vendor.conf" ]]; then + echo >> "${tmpdir}/vendor.conf" + fi + sort -Vru "${file}" >> "${tmpdir}/vendor.conf" +done + +mv "${tmpdir}/vendor.conf" vendor.conf + +echo "Please commit the change made by this file..." diff --git a/hack/sync-vendor.sh b/hack/sync-vendor.sh new file mode 100755 index 000000000..c5feaf6cd --- /dev/null +++ b/hack/sync-vendor.sh @@ -0,0 +1,59 @@ +#!/bin/bash + +# Copyright The containerd 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 + +# http://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. + +set -o errexit +set -o nounset +set -o pipefail + +source $(dirname "${BASH_SOURCE[0]}")/utils.sh +cd ${ROOT} + +echo "Compare vendor with containerd vendors..." +containerd_vendor=$(mktemp /tmp/containerd-vendor.conf.XXXX) +from-vendor CONTAINERD github.com/containerd/containerd +curl -s https://raw.githubusercontent.com/${CONTAINERD_REPO#*/}/${CONTAINERD_VERSION}/vendor.conf > ${containerd_vendor} +# Create a temporary vendor file to update. +tmp_vendor=$(mktemp /tmp/vendor.conf.XXXX) +while read vendor; do + repo=$(echo ${vendor} | awk '{print $1}') + commit=$(echo ${vendor} | awk '{print $2}') + alias=$(echo ${vendor} | awk '{print $3}') + vendor_in_containerd=$(grep ${repo} ${containerd_vendor} || true) + if [ -z "${vendor_in_containerd}" ]; then + echo ${vendor} >> ${tmp_vendor} + continue + fi + commit_in_containerd=$(echo ${vendor_in_containerd} | awk '{print $2}') + alias_in_containerd=$(echo ${vendor_in_containerd} | awk '{print $3}') + if [[ "${commit}" != "${commit_in_containerd}" || "${alias}" != "${alias_in_containerd}" ]]; then + echo ${vendor_in_containerd} >> ${tmp_vendor} + else + echo ${vendor} >> ${tmp_vendor} + fi +done < vendor.conf +# Update vendors if temporary vendor.conf is different from the original one. +if ! diff vendor.conf ${tmp_vendor} > /dev/null; then + if [ $# -gt 0 ] && [ ${1} = "-only-verify" ]; then + echo "Need to update vendor.conf." + diff vendor.conf ${tmp_vendor} + rm ${tmp_vendor} + exit 1 + else + echo "Updating vendor.conf." + mv ${tmp_vendor} vendor.conf + fi +fi +rm ${containerd_vendor} diff --git a/hack/test-cri.sh b/hack/test-cri.sh new file mode 100755 index 000000000..3667bece8 --- /dev/null +++ b/hack/test-cri.sh @@ -0,0 +1,69 @@ +#!/bin/bash + +# Copyright The containerd 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 + +# http://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. + +set -o nounset +set -o pipefail + +source $(dirname "${BASH_SOURCE[0]}")/test-utils.sh + +# FOCUS focuses the test to run. +FOCUS=${FOCUS:-} +# SKIP skips the test to skip. +SKIP=${SKIP:-""} +# REPORT_DIR is the the directory to store test logs. +REPORT_DIR=${REPORT_DIR:-"/tmp/test-cri"} +# RUNTIME is the runtime handler to use in the test. +RUNTIME=${RUNTIME:-""} + +# Check GOPATH +if [[ -z "${GOPATH}" ]]; then + echo "GOPATH is not set" + exit 1 +fi + +# For multiple GOPATHs, keep the first one only +GOPATH=${GOPATH%%:*} + +CRITEST=${GOPATH}/bin/critest + +GINKGO_PKG=github.com/onsi/ginkgo/ginkgo + +# Install ginkgo +if [ ! -x "$(command -v ginkgo)" ]; then + go get -u ${GINKGO_PKG} +fi + +# Install critest +if [ ! -x "$(command -v ${CRITEST})" ]; then + go get -d ${CRITOOL_PKG}/... + cd ${GOPATH}/src/${CRITOOL_PKG} + git fetch --all + git checkout ${CRITOOL_VERSION} + make critest + make install-critest -e BINDIR="${GOPATH}/bin" +fi +which ${CRITEST} + +mkdir -p ${REPORT_DIR} +test_setup ${REPORT_DIR} + +# Run cri validation test +sudo env PATH=${PATH} GOPATH=${GOPATH} ${CRITEST} --runtime-endpoint=${CONTAINERD_SOCK} --ginkgo.focus="${FOCUS}" --ginkgo.skip="${SKIP}" --parallel=8 --runtime-handler=${RUNTIME} +test_exit_code=$? + +test_teardown + +exit ${test_exit_code} diff --git a/hack/test-e2e-node.sh b/hack/test-e2e-node.sh new file mode 100755 index 000000000..9038a7f22 --- /dev/null +++ b/hack/test-e2e-node.sh @@ -0,0 +1,104 @@ +#!/bin/bash + +# Copyright The containerd 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 + +# http://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. + +set -o nounset +set -o pipefail + +source $(dirname "${BASH_SOURCE[0]}")/test-utils.sh + +DEFAULT_SKIP="\[Flaky\]|\[Slow\]|\[Serial\]" +DEFAULT_SKIP+="|querying\s\/stats\/summary" + +# FOCUS focuses the test to run. +export FOCUS=${FOCUS:-""} +# SKIP skips the test to skip. +export SKIP=${SKIP:-${DEFAULT_SKIP}} +# REPORT_DIR is the the directory to store test logs. +REPORT_DIR=${REPORT_DIR:-"/tmp/test-e2e-node"} +# UPLOAD_LOG indicates whether to upload test log to gcs. +UPLOAD_LOG=${UPLOAD_LOG:-false} +# TIMEOUT is the timeout of the test. +TIMEOUT=${TIMEOUT:-"40m"} +# FAIL_SWAP_ON makes kubelet fail when swap is on. +# Many dev environments run with swap on, so we don't fail by default. +FAIL_SWAP_ON=${FAIL_SWAP_ON:-"false"} + +# Check GOPATH +if [[ -z "${GOPATH}" ]]; then + echo "GOPATH is not set" + exit 1 +fi + +ORIGINAL_RULES=`mktemp` +sudo iptables-save > ${ORIGINAL_RULES} + +# Update ip firewall +# We need to add rules to accept all TCP/UDP/ICMP packets. +if sudo iptables -L INPUT | grep "Chain INPUT (policy DROP)" > /dev/null; then + sudo iptables -A INPUT -w -p TCP -j ACCEPT + sudo iptables -A INPUT -w -p UDP -j ACCEPT + sudo iptables -A INPUT -w -p ICMP -j ACCEPT +fi +if sudo iptables -L FORWARD | grep "Chain FORWARD (policy DROP)" > /dev/null; then + sudo iptables -A FORWARD -w -p TCP -j ACCEPT + sudo iptables -A FORWARD -w -p UDP -j ACCEPT + sudo iptables -A FORWARD -w -p ICMP -j ACCEPT +fi + +# For multiple GOPATHs, keep the first one only +GOPATH=${GOPATH%%:*} + +# Get kubernetes +KUBERNETES_REPO="https://github.com/kubernetes/kubernetes" +KUBERNETES_PATH="${GOPATH}/src/k8s.io/kubernetes" +if [ ! -d "${KUBERNETES_PATH}" ]; then + mkdir -p ${KUBERNETES_PATH} + cd ${KUBERNETES_PATH} + git clone https://${KUBERNETES_REPO} . +fi +cd ${KUBERNETES_PATH} +git fetch --all +git checkout ${KUBERNETES_VERSION} + +mkdir -p ${REPORT_DIR} +test_setup ${REPORT_DIR} + +timeout "${TIMEOUT}" make test-e2e-node \ + RUNTIME=remote \ + CONTAINER_RUNTIME_ENDPOINT=unix://${CONTAINERD_SOCK} \ + ARTIFACTS=${REPORT_DIR} \ + TEST_ARGS='--kubelet-flags=--cgroups-per-qos=true \ + --kubelet-flags=--cgroup-root=/ \ + --kubelet-flags=--fail-swap-on='${FAIL_SWAP_ON}' \ + --prepull-images=false' +test_exit_code=$? + +test_teardown + +sudo iptables-restore < ${ORIGINAL_RULES} +rm ${ORIGINAL_RULES} + +# UPLOAD_LOG_PATH is bucket to upload test logs. +UPLOAD_LOG_PATH=cri-containerd_test-e2e-node +if ${UPLOAD_LOG}; then + if [ -z "${VERSION}" ]; then + echo "VERSION is not set" + exit 1 + fi + upload_logs_to_gcs "${UPLOAD_LOG_PATH}" "${VERSION}-$(date +%Y%m%d-%H%M%S)" "${REPORT_DIR}" +fi + +exit ${test_exit_code} diff --git a/hack/test-integration.sh b/hack/test-integration.sh new file mode 100755 index 000000000..820d030ef --- /dev/null +++ b/hack/test-integration.sh @@ -0,0 +1,46 @@ +#!/bin/bash + +# Copyright The containerd 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 + +# http://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. + +set -o nounset +set -o pipefail + +source $(dirname "${BASH_SOURCE[0]}")/test-utils.sh +cd ${ROOT} + +# FOCUS focuses the test to run. +FOCUS=${FOCUS:-""} +# REPORT_DIR is the the directory to store test logs. +REPORT_DIR=${REPORT_DIR:-"/tmp/test-integration"} +# RUNTIME is the runtime handler to use in the test. +RUNTIME=${RUNTIME:-""} + +CRI_ROOT="${CONTAINERD_ROOT}/io.containerd.grpc.v1.cri" + +mkdir -p ${REPORT_DIR} +test_setup ${REPORT_DIR} + +# Run integration test. +sudo PATH=${PATH} ${ROOT}/_output/integration.test --test.run="${FOCUS}" --test.v \ + --cri-endpoint=${CONTAINERD_SOCK} \ + --cri-root=${CRI_ROOT} \ + --runtime-handler=${RUNTIME} \ + --containerd-bin=${CONTAINERD_BIN} + +test_exit_code=$? + +test_teardown + +exit ${test_exit_code} diff --git a/hack/test-utils.sh b/hack/test-utils.sh new file mode 100755 index 000000000..bde573b88 --- /dev/null +++ b/hack/test-utils.sh @@ -0,0 +1,119 @@ +#!/bin/bash + +# Copyright The containerd 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 + +# http://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. + +source $(dirname "${BASH_SOURCE[0]}")/utils.sh + +# RESTART_WAIT_PERIOD is the period to wait before restarting containerd. +RESTART_WAIT_PERIOD=${RESTART_WAIT_PERIOD:-10} +# CONTAINERD_FLAGS contains all containerd flags. +CONTAINERD_FLAGS="--log-level=debug " + +# Use a configuration file for containerd. +CONTAINERD_CONFIG_FILE=${CONTAINERD_CONFIG_FILE:-""} +if [ -z "${CONTAINERD_CONFIG_FILE}" ] && command -v sestatus >/dev/null 2>&1; then + selinux_config="/tmp/containerd-config-selinux.toml" + cat >${selinux_config} <<<' +[plugins.cri] + enable_selinux = true +' + CONTAINERD_CONFIG_FILE=${CONTAINERD_CONFIG_FILE:-"${selinux_config}"} +fi + +# CONTAINERD_TEST_SUFFIX is the suffix appended to the root/state directory used +# by test containerd. +CONTAINERD_TEST_SUFFIX=${CONTAINERD_TEST_SUFFIX:-"-test"} +# The containerd root directory. +CONTAINERD_ROOT=${CONTAINERD_ROOT:-"/var/lib/containerd${CONTAINERD_TEST_SUFFIX}"} +# The containerd state directory. +CONTAINERD_STATE=${CONTAINERD_STATE:-"/run/containerd${CONTAINERD_TEST_SUFFIX}"} +# The containerd socket address. +CONTAINERD_SOCK=${CONTAINERD_SOCK:-unix://${CONTAINERD_STATE}/containerd.sock} +# The containerd binary name. +CONTAINERD_BIN=${CONTAINERD_BIN:-"containerd${CONTAINERD_TEST_SUFFIX}"} +if [ -f "${CONTAINERD_CONFIG_FILE}" ]; then + CONTAINERD_FLAGS+="--config ${CONTAINERD_CONFIG_FILE} " +fi +CONTAINERD_FLAGS+="--address ${CONTAINERD_SOCK#"unix://"} \ + --state ${CONTAINERD_STATE} \ + --root ${CONTAINERD_ROOT}" + +containerd_groupid= + +# test_setup starts containerd. +test_setup() { + local report_dir=$1 + # Start containerd + if [ ! -x "${ROOT}/_output/containerd" ]; then + echo "containerd is not built" + exit 1 + fi + # rename the test containerd binary, so that we can easily + # distinguish it. + cp ${ROOT}/_output/containerd ${ROOT}/_output/${CONTAINERD_BIN} + set -m + # Create containerd in a different process group + # so that we can easily clean them up. + keepalive "sudo PATH=${PATH} ${ROOT}/_output/${CONTAINERD_BIN} ${CONTAINERD_FLAGS}" \ + ${RESTART_WAIT_PERIOD} &> ${report_dir}/containerd.log & + pid=$! + set +m + containerd_groupid=$(ps -o pgid= -p ${pid}) + # Wait for containerd to be running by using the containerd client ctr to check the version + # of the containerd server. Wait an increasing amount of time after each of five attempts + local -r ctr_path=$(which ctr) + if [ -z "${ctr_path}" ]; then + echo "ctr is not in PATH" + exit 1 + fi + local -r crictl_path=$(which crictl) + if [ -z "${crictl_path}" ]; then + echo "crictl is not in PATH" + exit 1 + fi + readiness_check "sudo ${ctr_path} --address ${CONTAINERD_SOCK#"unix://"} version" + readiness_check "sudo ${crictl_path} --runtime-endpoint=${CONTAINERD_SOCK} info" +} + +# test_teardown kills containerd. +test_teardown() { + if [ -n "${containerd_groupid}" ]; then + sudo pkill -g ${containerd_groupid} + fi +} + +# keepalive runs a command and keeps it alive. +# keepalive process is eventually killed in test_teardown. +keepalive() { + local command=$1 + echo ${command} + local wait_period=$2 + while true; do + ${command} + sleep ${wait_period} + done +} + +# readiness_check checks readiness of a daemon with specified command. +readiness_check() { + local command=$1 + local MAX_ATTEMPTS=5 + local attempt_num=1 + until ${command} &> /dev/null || (( attempt_num == MAX_ATTEMPTS )) + do + echo "$attempt_num attempt \"$command\"! Trying again in $attempt_num seconds..." + sleep $(( attempt_num++ )) + done +} diff --git a/hack/update-proto.sh b/hack/update-proto.sh new file mode 100755 index 000000000..505bb8d6c --- /dev/null +++ b/hack/update-proto.sh @@ -0,0 +1,44 @@ +#!/bin/bash + +# Copyright The containerd 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 + +# http://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. + +set -o errexit +set -o nounset +set -o pipefail + +ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"/.. +API_ROOT="${ROOT}/${API_PATH-"pkg/api/v1"}" + +go get k8s.io/code-generator/cmd/go-to-protobuf/protoc-gen-gogo +if ! which protoc-gen-gogo >/dev/null; then + echo "GOPATH is not in PATH" + exit 1 +fi + +function cleanup { + rm -f ${API_ROOT}/api.pb.go.bak +} + +trap cleanup EXIT + +protoc \ + --proto_path="${API_ROOT}" \ + --proto_path="${ROOT}/vendor" \ + --gogo_out=plugins=grpc:${API_ROOT} ${API_ROOT}/api.proto + +# Update boilerplate for the generated file. +echo "$(cat hack/boilerplate/boilerplate ${API_ROOT}/api.pb.go)" > ${API_ROOT}/api.pb.go + +gofmt -l -s -w ${API_ROOT}/api.pb.go diff --git a/hack/utils.sh b/hack/utils.sh new file mode 100755 index 000000000..13a6a3ff5 --- /dev/null +++ b/hack/utils.sh @@ -0,0 +1,105 @@ +#!/bin/bash + +# Copyright The containerd 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 + +# http://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. + +ROOT="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"/.. + +# Not from vendor.conf. +KUBERNETES_VERSION="v1.19.0-beta.2" +CRITOOL_VERSION=${CRITOOL_VERSION:-baca4a152dfe671fc17911a7af74bcb61680ee39} +CRITOOL_PKG=github.com/kubernetes-sigs/cri-tools +CRITOOL_REPO=github.com/kubernetes-sigs/cri-tools + +# VENDOR is the path to vendor.conf. +VENDOR=${VENDOR:-"${ROOT}/vendor.conf"} + +# upload_logs_to_gcs uploads test logs to gcs. +# Var set: +# 1. Bucket: gcs bucket to upload logs. +# 2. Dir: directory name to upload logs. +# 3. Test Result: directory of the test result. +upload_logs_to_gcs() { + local -r bucket=$1 + local -r dir=$2 + local -r result=$3 + if ! gsutil ls "gs://${bucket}" > /dev/null; then + create_ttl_bucket ${bucket} + fi + local -r upload_log_path=${bucket}/${dir} + gsutil cp -r "${result}" "gs://${upload_log_path}" + echo "Test logs are uploaed to: + http://gcsweb.k8s.io/gcs/${upload_log_path}/" +} + +# create_ttl_bucket create a public bucket in which all objects +# have a default TTL (30 days). +# Var set: +# 1. Bucket: gcs bucket name. +create_ttl_bucket() { + local -r bucket=$1 + gsutil mb "gs://${bucket}" + local -r bucket_rule=$(mktemp) + # Set 30 day TTL for logs inside the bucket. + echo '{"rule": [{"action": {"type": "Delete"},"condition": {"age": 30}}]}' > ${bucket_rule} + gsutil lifecycle set "${bucket_rule}" "gs://${bucket}" + rm "${bucket_rule}" + + gsutil -m acl ch -g all:R "gs://${bucket}" + gsutil defacl set public-read "gs://${bucket}" +} + +# sha256 generates a sha256 checksum for a file. +# Var set: +# 1. Filename. +sha256() { + if which sha256sum >/dev/null 2>&1; then + sha256sum "$1" | awk '{ print $1 }' + else + shasum -a256 "$1" | awk '{ print $1 }' + fi +} + +# Takes a prefix ($what) and a $repo and sets `$what_VERSION` and +# `$what_REPO` from vendor.conf, where `$what_REPO` defaults to $repo +# but is overridden by the 3rd field of vendor.conf. +from-vendor() { + local what=$1 + local repo=$2 + local vendor=$VENDOR + setvars=$(awk -v REPO=$repo -v WHAT=$what -- ' + BEGIN { rc=1 } # Assume we did not find what we were looking for. + // { + if ($1 == REPO) { + if ($3 != "" && $3 !~ /#.*/ ) { gsub(/http.*\/\//, "", $3); REPO = $3 }; # Override repo. + printf("%s_VERSION=%s; %s_REPO=%s\n", WHAT, $2, WHAT, REPO); + rc=0; # Note success for use in END block. + exit # No point looking further. + } + } + END { exit rc } # Exit with the desired code. + ' $vendor) + if [ $? -ne 0 ] ; then + echo "failed to get version of $repo from $vendor" >&2 + exit 1 + fi + eval $setvars +} + +# yaml-quote quotes something appropriate for a yaml string. +# This is the same with: +# https://github.com/kubernetes/kubernetes/blob/v1.10.1/cluster/gce/util.sh#L471. +yaml-quote() { + echo "'$(echo "${@:-}" | sed -e "s/'/''/g")'" +} diff --git a/hack/verify-gofmt.sh b/hack/verify-gofmt.sh new file mode 100755 index 000000000..60992016f --- /dev/null +++ b/hack/verify-gofmt.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +# Copyright The containerd 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 + +# http://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. + +set -o errexit +set -o nounset +set -o pipefail + +find_files() { + find . -not \( \ + \( \ + -wholename '*/vendor/*' \ + \) -prune \ + \) -name '*.go' +} + +GOFMT="gofmt -s" +bad_files=$(find_files | xargs $GOFMT -l) +if [[ -n "${bad_files}" ]]; then + echo "!!! '$GOFMT' needs to be run on the following files: " + echo "${bad_files}" + exit 1 +fi diff --git a/hack/verify-vendor.sh b/hack/verify-vendor.sh new file mode 100755 index 000000000..a095b80ae --- /dev/null +++ b/hack/verify-vendor.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash + +# Copyright The containerd 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 + +# http://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. + +set -o errexit +set -o nounset +set -o pipefail + +tmpdir="$(mktemp -d)" +trap "rm -rf ${tmpdir}" EXIT + +git clone "https://github.com/containerd/project" "${tmpdir}" +"${tmpdir}"/script/validate/vendor diff --git a/integration/addition_gids_test.go b/integration/addition_gids_test.go new file mode 100644 index 000000000..c984cf7ad --- /dev/null +++ b/integration/addition_gids_test.go @@ -0,0 +1,89 @@ +// +build linux + +/* + Copyright The containerd 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 + + http://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. +*/ + +package integration + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" +) + +func TestAdditionalGids(t *testing.T) { + testPodLogDir, err := ioutil.TempDir("/tmp", "additional-gids") + require.NoError(t, err) + defer os.RemoveAll(testPodLogDir) + + t.Log("Create a sandbox with log directory") + sbConfig := PodSandboxConfig("sandbox", "additional-gids", + WithPodLogDirectory(testPodLogDir)) + sb, err := runtimeService.RunPodSandbox(sbConfig, *runtimeHandler) + require.NoError(t, err) + defer func() { + assert.NoError(t, runtimeService.StopPodSandbox(sb)) + assert.NoError(t, runtimeService.RemovePodSandbox(sb)) + }() + + const ( + testImage = "busybox" + containerName = "test-container" + ) + t.Logf("Pull test image %q", testImage) + img, err := imageService.PullImage(&runtime.ImageSpec{Image: testImage}, nil, sbConfig) + require.NoError(t, err) + defer func() { + assert.NoError(t, imageService.RemoveImage(&runtime.ImageSpec{Image: img})) + }() + + t.Log("Create a container to print id") + cnConfig := ContainerConfig( + containerName, + "busybox", + WithCommand("id"), + WithLogPath(containerName), + WithSupplementalGroups([]int64{1 /*daemon*/, 1234 /*new group*/}), + ) + cn, err := runtimeService.CreateContainer(sb, cnConfig, sbConfig) + require.NoError(t, err) + + t.Log("Start the container") + require.NoError(t, runtimeService.StartContainer(cn)) + + t.Log("Wait for container to finish running") + require.NoError(t, Eventually(func() (bool, error) { + s, err := runtimeService.ContainerStatus(cn) + if err != nil { + return false, err + } + if s.GetState() == runtime.ContainerState_CONTAINER_EXITED { + return true, nil + } + return false, nil + }, time.Second, 30*time.Second)) + + t.Log("Search additional groups in container log") + content, err := ioutil.ReadFile(filepath.Join(testPodLogDir, containerName)) + assert.NoError(t, err) + assert.Contains(t, string(content), "groups=1(daemon),10(wheel),1234") +} diff --git a/integration/container_log_test.go b/integration/container_log_test.go new file mode 100644 index 000000000..c11c874bf --- /dev/null +++ b/integration/container_log_test.go @@ -0,0 +1,175 @@ +// +build linux + +/* + Copyright The containerd 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 + + http://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. +*/ + +package integration + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" +) + +func TestContainerLogWithoutTailingNewLine(t *testing.T) { + testPodLogDir, err := ioutil.TempDir("/tmp", "container-log-without-tailing-newline") + require.NoError(t, err) + defer os.RemoveAll(testPodLogDir) + + t.Log("Create a sandbox with log directory") + sbConfig := PodSandboxConfig("sandbox", "container-log-without-tailing-newline", + WithPodLogDirectory(testPodLogDir), + ) + sb, err := runtimeService.RunPodSandbox(sbConfig, *runtimeHandler) + require.NoError(t, err) + defer func() { + assert.NoError(t, runtimeService.StopPodSandbox(sb)) + assert.NoError(t, runtimeService.RemovePodSandbox(sb)) + }() + + const ( + testImage = "busybox" + containerName = "test-container" + ) + t.Logf("Pull test image %q", testImage) + img, err := imageService.PullImage(&runtime.ImageSpec{Image: testImage}, nil, sbConfig) + require.NoError(t, err) + defer func() { + assert.NoError(t, imageService.RemoveImage(&runtime.ImageSpec{Image: img})) + }() + + t.Log("Create a container with log path") + cnConfig := ContainerConfig( + containerName, + testImage, + WithCommand("sh", "-c", "printf abcd"), + WithLogPath(containerName), + ) + cn, err := runtimeService.CreateContainer(sb, cnConfig, sbConfig) + require.NoError(t, err) + + t.Log("Start the container") + require.NoError(t, runtimeService.StartContainer(cn)) + + t.Log("Wait for container to finish running") + require.NoError(t, Eventually(func() (bool, error) { + s, err := runtimeService.ContainerStatus(cn) + if err != nil { + return false, err + } + if s.GetState() == runtime.ContainerState_CONTAINER_EXITED { + return true, nil + } + return false, nil + }, time.Second, 30*time.Second)) + + t.Log("Check container log") + content, err := ioutil.ReadFile(filepath.Join(testPodLogDir, containerName)) + assert.NoError(t, err) + checkContainerLog(t, string(content), []string{ + fmt.Sprintf("%s %s %s", runtime.Stdout, runtime.LogTagPartial, "abcd"), + }) +} + +func TestLongContainerLog(t *testing.T) { + testPodLogDir, err := ioutil.TempDir("/tmp", "long-container-log") + require.NoError(t, err) + defer os.RemoveAll(testPodLogDir) + + t.Log("Create a sandbox with log directory") + sbConfig := PodSandboxConfig("sandbox", "long-container-log", + WithPodLogDirectory(testPodLogDir), + ) + sb, err := runtimeService.RunPodSandbox(sbConfig, *runtimeHandler) + require.NoError(t, err) + defer func() { + assert.NoError(t, runtimeService.StopPodSandbox(sb)) + assert.NoError(t, runtimeService.RemovePodSandbox(sb)) + }() + + const ( + testImage = "busybox" + containerName = "test-container" + ) + t.Logf("Pull test image %q", testImage) + img, err := imageService.PullImage(&runtime.ImageSpec{Image: testImage}, nil, sbConfig) + require.NoError(t, err) + defer func() { + assert.NoError(t, imageService.RemoveImage(&runtime.ImageSpec{Image: img})) + }() + + t.Log("Create a container with log path") + config, err := CRIConfig() + require.NoError(t, err) + maxSize := config.MaxContainerLogLineSize + shortLineCmd := fmt.Sprintf("i=0; while [ $i -lt %d ]; do printf %s; i=$((i+1)); done", maxSize-1, "a") + maxLenLineCmd := fmt.Sprintf("i=0; while [ $i -lt %d ]; do printf %s; i=$((i+1)); done", maxSize, "b") + longLineCmd := fmt.Sprintf("i=0; while [ $i -lt %d ]; do printf %s; i=$((i+1)); done", maxSize+1, "c") + cnConfig := ContainerConfig( + containerName, + testImage, + WithCommand("sh", "-c", + fmt.Sprintf("%s; echo; %s; echo; %s; echo", shortLineCmd, maxLenLineCmd, longLineCmd)), + WithLogPath(containerName), + ) + cn, err := runtimeService.CreateContainer(sb, cnConfig, sbConfig) + require.NoError(t, err) + + t.Log("Start the container") + require.NoError(t, runtimeService.StartContainer(cn)) + + t.Log("Wait for container to finish running") + require.NoError(t, Eventually(func() (bool, error) { + s, err := runtimeService.ContainerStatus(cn) + if err != nil { + return false, err + } + if s.GetState() == runtime.ContainerState_CONTAINER_EXITED { + return true, nil + } + return false, nil + }, time.Second, 30*time.Second)) + + t.Log("Check container log") + content, err := ioutil.ReadFile(filepath.Join(testPodLogDir, containerName)) + assert.NoError(t, err) + checkContainerLog(t, string(content), []string{ + fmt.Sprintf("%s %s %s", runtime.Stdout, runtime.LogTagFull, strings.Repeat("a", maxSize-1)), + fmt.Sprintf("%s %s %s", runtime.Stdout, runtime.LogTagFull, strings.Repeat("b", maxSize)), + fmt.Sprintf("%s %s %s", runtime.Stdout, runtime.LogTagPartial, strings.Repeat("c", maxSize)), + fmt.Sprintf("%s %s %s", runtime.Stdout, runtime.LogTagFull, "c"), + }) +} + +func checkContainerLog(t *testing.T, log string, messages []string) { + lines := strings.Split(strings.TrimSpace(log), "\n") + require.Len(t, lines, len(messages), "log line number should match") + for i, line := range lines { + parts := strings.SplitN(line, " ", 2) + require.Len(t, parts, 2) + _, err := time.Parse(time.RFC3339Nano, parts[0]) + assert.NoError(t, err, "timestamp should be in RFC3339Nano format") + assert.Equal(t, messages[i], parts[1], "log content should match") + } +} diff --git a/integration/container_restart_test.go b/integration/container_restart_test.go new file mode 100644 index 000000000..bce558e88 --- /dev/null +++ b/integration/container_restart_test.go @@ -0,0 +1,62 @@ +// +build linux + +/* + Copyright The containerd 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 + + http://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. +*/ + +package integration + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// Test to verify container can be restarted +func TestContainerRestart(t *testing.T) { + t.Logf("Create a pod config and run sandbox container") + sbConfig := PodSandboxConfig("sandbox1", "restart") + sb, err := runtimeService.RunPodSandbox(sbConfig, *runtimeHandler) + require.NoError(t, err) + defer func() { + assert.NoError(t, runtimeService.StopPodSandbox(sb)) + assert.NoError(t, runtimeService.RemovePodSandbox(sb)) + }() + t.Logf("Create a container config and run container in a pod") + containerConfig := ContainerConfig( + "container1", + pauseImage, + WithTestLabels(), + WithTestAnnotations(), + ) + cn, err := runtimeService.CreateContainer(sb, containerConfig, sbConfig) + require.NoError(t, err) + defer func() { + assert.NoError(t, runtimeService.RemoveContainer(cn)) + }() + require.NoError(t, runtimeService.StartContainer(cn)) + defer func() { + assert.NoError(t, runtimeService.StopContainer(cn, 10)) + }() + + t.Logf("Restart the container with same config") + require.NoError(t, runtimeService.StopContainer(cn, 10)) + require.NoError(t, runtimeService.RemoveContainer(cn)) + + cn, err = runtimeService.CreateContainer(sb, containerConfig, sbConfig) + require.NoError(t, err) + require.NoError(t, runtimeService.StartContainer(cn)) +} diff --git a/integration/container_stats_test.go b/integration/container_stats_test.go new file mode 100644 index 000000000..6752b523f --- /dev/null +++ b/integration/container_stats_test.go @@ -0,0 +1,347 @@ +// +build linux + +/* + Copyright The containerd 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 + + http://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. +*/ + +package integration + +import ( + "fmt" + "testing" + "time" + + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" +) + +// Test to verify for a container ID +func TestContainerStats(t *testing.T) { + t.Logf("Create a pod config and run sandbox container") + sbConfig := PodSandboxConfig("sandbox1", "stats") + sb, err := runtimeService.RunPodSandbox(sbConfig, *runtimeHandler) + require.NoError(t, err) + defer func() { + assert.NoError(t, runtimeService.StopPodSandbox(sb)) + assert.NoError(t, runtimeService.RemovePodSandbox(sb)) + }() + t.Logf("Create a container config and run container in a pod") + containerConfig := ContainerConfig( + "container1", + pauseImage, + WithTestLabels(), + WithTestAnnotations(), + ) + cn, err := runtimeService.CreateContainer(sb, containerConfig, sbConfig) + require.NoError(t, err) + defer func() { + assert.NoError(t, runtimeService.RemoveContainer(cn)) + }() + require.NoError(t, runtimeService.StartContainer(cn)) + defer func() { + assert.NoError(t, runtimeService.StopContainer(cn, 10)) + }() + + t.Logf("Fetch stats for container") + var s *runtime.ContainerStats + require.NoError(t, Eventually(func() (bool, error) { + s, err = runtimeService.ContainerStats(cn) + if err != nil { + return false, err + } + if s.GetWritableLayer().GetUsedBytes().GetValue() != 0 && + s.GetWritableLayer().GetInodesUsed().GetValue() != 0 { + return true, nil + } + return false, nil + }, time.Second, 30*time.Second)) + + t.Logf("Verify stats received for container %q", cn) + testStats(t, s, containerConfig) +} + +// Test to verify filtering without any filter +func TestContainerListStats(t *testing.T) { + t.Logf("Create a pod config and run sandbox container") + sbConfig := PodSandboxConfig("running-pod", "statsls") + sb, err := runtimeService.RunPodSandbox(sbConfig, *runtimeHandler) + require.NoError(t, err) + defer func() { + assert.NoError(t, runtimeService.StopPodSandbox(sb)) + assert.NoError(t, runtimeService.RemovePodSandbox(sb)) + }() + t.Logf("Create a container config and run containers in a pod") + containerConfigMap := make(map[string]*runtime.ContainerConfig) + for i := 0; i < 3; i++ { + cName := fmt.Sprintf("container%d", i) + containerConfig := ContainerConfig( + cName, + pauseImage, + WithTestLabels(), + WithTestAnnotations(), + ) + cn, err := runtimeService.CreateContainer(sb, containerConfig, sbConfig) + require.NoError(t, err) + containerConfigMap[cn] = containerConfig + defer func() { + assert.NoError(t, runtimeService.RemoveContainer(cn)) + }() + require.NoError(t, runtimeService.StartContainer(cn)) + defer func() { + assert.NoError(t, runtimeService.StopContainer(cn, 10)) + }() + } + + t.Logf("Fetch all container stats") + var stats []*runtime.ContainerStats + require.NoError(t, Eventually(func() (bool, error) { + stats, err = runtimeService.ListContainerStats(&runtime.ContainerStatsFilter{}) + if err != nil { + return false, err + } + for _, s := range stats { + if s.GetWritableLayer().GetUsedBytes().GetValue() == 0 && + s.GetWritableLayer().GetInodesUsed().GetValue() == 0 { + return false, nil + } + } + return true, nil + }, time.Second, 30*time.Second)) + + t.Logf("Verify all container stats") + for _, s := range stats { + testStats(t, s, containerConfigMap[s.GetAttributes().GetId()]) + } +} + +// Test to verify filtering given a specific container ID +// TODO Convert the filter tests into table driven tests and unit tests +func TestContainerListStatsWithIdFilter(t *testing.T) { + t.Logf("Create a pod config and run sandbox container") + sbConfig := PodSandboxConfig("running-pod", "statsls") + sb, err := runtimeService.RunPodSandbox(sbConfig, *runtimeHandler) + require.NoError(t, err) + defer func() { + assert.NoError(t, runtimeService.StopPodSandbox(sb)) + assert.NoError(t, runtimeService.RemovePodSandbox(sb)) + }() + t.Logf("Create a container config and run containers in a pod") + containerConfigMap := make(map[string]*runtime.ContainerConfig) + for i := 0; i < 3; i++ { + cName := fmt.Sprintf("container%d", i) + containerConfig := ContainerConfig( + cName, + pauseImage, + WithTestLabels(), + WithTestAnnotations(), + ) + cn, err := runtimeService.CreateContainer(sb, containerConfig, sbConfig) + containerConfigMap[cn] = containerConfig + require.NoError(t, err) + defer func() { + assert.NoError(t, runtimeService.RemoveContainer(cn)) + }() + require.NoError(t, runtimeService.StartContainer(cn)) + defer func() { + assert.NoError(t, runtimeService.StopContainer(cn, 10)) + }() + } + + t.Logf("Fetch container stats for each container with Filter") + var stats []*runtime.ContainerStats + for id := range containerConfigMap { + require.NoError(t, Eventually(func() (bool, error) { + stats, err = runtimeService.ListContainerStats( + &runtime.ContainerStatsFilter{Id: id}) + if err != nil { + return false, err + } + if len(stats) != 1 { + return false, errors.New("unexpected stats length") + } + if stats[0].GetWritableLayer().GetUsedBytes().GetValue() != 0 && + stats[0].GetWritableLayer().GetInodesUsed().GetValue() != 0 { + return true, nil + } + return false, nil + }, time.Second, 30*time.Second)) + + t.Logf("Verify container stats for %s", id) + for _, s := range stats { + require.Equal(t, s.GetAttributes().GetId(), id) + testStats(t, s, containerConfigMap[id]) + } + } +} + +// Test to verify filtering given a specific Sandbox ID. Stats for +// all the containers in a pod should be returned +func TestContainerListStatsWithSandboxIdFilter(t *testing.T) { + t.Logf("Create a pod config and run sandbox container") + sbConfig := PodSandboxConfig("running-pod", "statsls") + sb, err := runtimeService.RunPodSandbox(sbConfig, *runtimeHandler) + require.NoError(t, err) + defer func() { + assert.NoError(t, runtimeService.StopPodSandbox(sb)) + assert.NoError(t, runtimeService.RemovePodSandbox(sb)) + }() + t.Logf("Create a container config and run containers in a pod") + containerConfigMap := make(map[string]*runtime.ContainerConfig) + for i := 0; i < 3; i++ { + cName := fmt.Sprintf("container%d", i) + containerConfig := ContainerConfig( + cName, + pauseImage, + WithTestLabels(), + WithTestAnnotations(), + ) + cn, err := runtimeService.CreateContainer(sb, containerConfig, sbConfig) + containerConfigMap[cn] = containerConfig + require.NoError(t, err) + defer func() { + assert.NoError(t, runtimeService.RemoveContainer(cn)) + }() + require.NoError(t, runtimeService.StartContainer(cn)) + defer func() { + assert.NoError(t, runtimeService.StopContainer(cn, 10)) + }() + } + + t.Logf("Fetch container stats for each container with Filter") + var stats []*runtime.ContainerStats + require.NoError(t, Eventually(func() (bool, error) { + stats, err = runtimeService.ListContainerStats( + &runtime.ContainerStatsFilter{PodSandboxId: sb}) + if err != nil { + return false, err + } + if len(stats) != 3 { + return false, errors.New("unexpected stats length") + } + if stats[0].GetWritableLayer().GetUsedBytes().GetValue() != 0 && + stats[0].GetWritableLayer().GetInodesUsed().GetValue() != 0 { + return true, nil + } + return false, nil + }, time.Second, 30*time.Second)) + t.Logf("Verify container stats for sandbox %q", sb) + for _, s := range stats { + testStats(t, s, containerConfigMap[s.GetAttributes().GetId()]) + } +} + +// Test to verify filtering given a specific container ID and +// sandbox ID +func TestContainerListStatsWithIdSandboxIdFilter(t *testing.T) { + t.Logf("Create a pod config and run sandbox container") + sbConfig := PodSandboxConfig("running-pod", "statsls") + sb, err := runtimeService.RunPodSandbox(sbConfig, *runtimeHandler) + require.NoError(t, err) + defer func() { + assert.NoError(t, runtimeService.StopPodSandbox(sb)) + assert.NoError(t, runtimeService.RemovePodSandbox(sb)) + }() + t.Logf("Create container config and run containers in a pod") + containerConfigMap := make(map[string]*runtime.ContainerConfig) + for i := 0; i < 3; i++ { + cName := fmt.Sprintf("container%d", i) + containerConfig := ContainerConfig( + cName, + pauseImage, + WithTestLabels(), + WithTestAnnotations(), + ) + cn, err := runtimeService.CreateContainer(sb, containerConfig, sbConfig) + containerConfigMap[cn] = containerConfig + require.NoError(t, err) + defer func() { + assert.NoError(t, runtimeService.RemoveContainer(cn)) + }() + require.NoError(t, runtimeService.StartContainer(cn)) + defer func() { + assert.NoError(t, runtimeService.StopContainer(cn, 10)) + }() + } + t.Logf("Fetch container stats for sandbox ID and container ID filter") + var stats []*runtime.ContainerStats + for id, config := range containerConfigMap { + require.NoError(t, Eventually(func() (bool, error) { + stats, err = runtimeService.ListContainerStats( + &runtime.ContainerStatsFilter{Id: id, PodSandboxId: sb}) + if err != nil { + return false, err + } + if len(stats) != 1 { + return false, errors.New("unexpected stats length") + } + if stats[0].GetWritableLayer().GetUsedBytes().GetValue() != 0 && + stats[0].GetWritableLayer().GetInodesUsed().GetValue() != 0 { + return true, nil + } + return false, nil + }, time.Second, 30*time.Second)) + t.Logf("Verify container stats for sandbox %q and container %q filter", sb, id) + for _, s := range stats { + testStats(t, s, config) + } + } + + t.Logf("Fetch container stats for sandbox truncID and container truncID filter ") + for id, config := range containerConfigMap { + require.NoError(t, Eventually(func() (bool, error) { + stats, err = runtimeService.ListContainerStats( + &runtime.ContainerStatsFilter{Id: id[:3], PodSandboxId: sb[:3]}) + if err != nil { + return false, err + } + if len(stats) != 1 { + return false, errors.New("unexpected stats length") + } + if stats[0].GetWritableLayer().GetUsedBytes().GetValue() != 0 && + stats[0].GetWritableLayer().GetInodesUsed().GetValue() != 0 { + return true, nil + } + return false, nil + }, time.Second, 30*time.Second)) + t.Logf("Verify container stats for sandbox %q and container %q filter", sb, id) + for _, s := range stats { + testStats(t, s, config) + } + } +} + +// TODO make this as options to use for dead container tests +func testStats(t *testing.T, + s *runtime.ContainerStats, + config *runtime.ContainerConfig, +) { + require.NotEmpty(t, s.GetAttributes().GetId()) + require.NotEmpty(t, s.GetAttributes().GetMetadata()) + require.NotEmpty(t, s.GetAttributes().GetAnnotations()) + require.Equal(t, s.GetAttributes().GetLabels(), config.Labels) + require.Equal(t, s.GetAttributes().GetAnnotations(), config.Annotations) + require.Equal(t, s.GetAttributes().GetMetadata().Name, config.Metadata.Name) + require.NotEmpty(t, s.GetAttributes().GetLabels()) + require.NotEmpty(t, s.GetCpu().GetTimestamp()) + require.NotEmpty(t, s.GetCpu().GetUsageCoreNanoSeconds().GetValue()) + require.NotEmpty(t, s.GetMemory().GetTimestamp()) + require.NotEmpty(t, s.GetMemory().GetWorkingSetBytes().GetValue()) + require.NotEmpty(t, s.GetWritableLayer().GetTimestamp()) + require.NotEmpty(t, s.GetWritableLayer().GetFsId().GetMountpoint()) + require.NotEmpty(t, s.GetWritableLayer().GetUsedBytes().GetValue()) + require.NotEmpty(t, s.GetWritableLayer().GetInodesUsed().GetValue()) +} diff --git a/integration/container_stop_test.go b/integration/container_stop_test.go new file mode 100644 index 000000000..b270ed54b --- /dev/null +++ b/integration/container_stop_test.go @@ -0,0 +1,141 @@ +// +build linux + +/* + Copyright The containerd 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 + + http://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. +*/ + +package integration + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" +) + +func TestSharedPidMultiProcessContainerStop(t *testing.T) { + for name, sbConfig := range map[string]*runtime.PodSandboxConfig{ + "hostpid": PodSandboxConfig("sandbox", "host-pid-container-stop", WithHostPid), + "podpid": PodSandboxConfig("sandbox", "pod-pid-container-stop", WithPodPid), + } { + t.Run(name, func(t *testing.T) { + t.Log("Create a shared pid sandbox") + sb, err := runtimeService.RunPodSandbox(sbConfig, *runtimeHandler) + require.NoError(t, err) + defer func() { + assert.NoError(t, runtimeService.StopPodSandbox(sb)) + assert.NoError(t, runtimeService.RemovePodSandbox(sb)) + }() + + const ( + testImage = "busybox" + containerName = "test-container" + ) + t.Logf("Pull test image %q", testImage) + img, err := imageService.PullImage(&runtime.ImageSpec{Image: testImage}, nil, sbConfig) + require.NoError(t, err) + defer func() { + assert.NoError(t, imageService.RemoveImage(&runtime.ImageSpec{Image: img})) + }() + + t.Log("Create a multi-process container") + cnConfig := ContainerConfig( + containerName, + testImage, + WithCommand("sh", "-c", "sleep 10000 & sleep 10000"), + ) + cn, err := runtimeService.CreateContainer(sb, cnConfig, sbConfig) + require.NoError(t, err) + + t.Log("Start the container") + require.NoError(t, runtimeService.StartContainer(cn)) + + t.Log("Stop the container") + require.NoError(t, runtimeService.StopContainer(cn, 0)) + + t.Log("The container state should be exited") + s, err := runtimeService.ContainerStatus(cn) + require.NoError(t, err) + assert.Equal(t, s.GetState(), runtime.ContainerState_CONTAINER_EXITED) + }) + } +} + +func TestContainerStopCancellation(t *testing.T) { + t.Log("Create a pod sandbox") + sbConfig := PodSandboxConfig("sandbox", "cancel-container-stop") + sb, err := runtimeService.RunPodSandbox(sbConfig, *runtimeHandler) + require.NoError(t, err) + defer func() { + assert.NoError(t, runtimeService.StopPodSandbox(sb)) + assert.NoError(t, runtimeService.RemovePodSandbox(sb)) + }() + + const ( + testImage = "busybox" + containerName = "test-container" + ) + t.Logf("Pull test image %q", testImage) + img, err := imageService.PullImage(&runtime.ImageSpec{Image: testImage}, nil, sbConfig) + require.NoError(t, err) + defer func() { + assert.NoError(t, imageService.RemoveImage(&runtime.ImageSpec{Image: img})) + }() + + t.Log("Create a container which traps sigterm") + cnConfig := ContainerConfig( + containerName, + testImage, + WithCommand("sh", "-c", `trap "echo ignore sigterm" TERM; sleep 1000`), + ) + cn, err := runtimeService.CreateContainer(sb, cnConfig, sbConfig) + require.NoError(t, err) + + t.Log("Start the container") + require.NoError(t, runtimeService.StartContainer(cn)) + + t.Log("Stop the container with 3s timeout, but 1s context timeout") + // Note that with container pid namespace, the sleep process + // is pid 1, and SIGTERM sent by `StopContainer` will be ignored. + rawClient, err := RawRuntimeClient() + require.NoError(t, err) + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + defer cancel() + _, err = rawClient.StopContainer(ctx, &runtime.StopContainerRequest{ + ContainerId: cn, + Timeout: 3, + }) + assert.Error(t, err) + + t.Log("The container should still be running even after 5 seconds") + assert.NoError(t, Consistently(func() (bool, error) { + s, err := runtimeService.ContainerStatus(cn) + if err != nil { + return false, err + } + return s.GetState() == runtime.ContainerState_CONTAINER_RUNNING, nil + }, 100*time.Millisecond, 5*time.Second)) + + t.Log("Stop the container with 1s timeout, without shorter context timeout") + assert.NoError(t, runtimeService.StopContainer(cn, 1)) + + t.Log("The container state should be exited") + s, err := runtimeService.ContainerStatus(cn) + require.NoError(t, err) + assert.Equal(t, s.GetState(), runtime.ContainerState_CONTAINER_EXITED) +} diff --git a/integration/container_update_resources_test.go b/integration/container_update_resources_test.go new file mode 100644 index 000000000..83850e0b7 --- /dev/null +++ b/integration/container_update_resources_test.go @@ -0,0 +1,107 @@ +// +build linux + +/* + Copyright The containerd 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 + + http://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. +*/ + +package integration + +import ( + "testing" + + "github.com/containerd/cgroups" + runtimespec "github.com/opencontainers/runtime-spec/specs-go" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/net/context" + runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" +) + +func checkMemoryLimit(t *testing.T, spec *runtimespec.Spec, memLimit int64) { + require.NotNil(t, spec) + require.NotNil(t, spec.Linux) + require.NotNil(t, spec.Linux.Resources) + require.NotNil(t, spec.Linux.Resources.Memory) + require.NotNil(t, spec.Linux.Resources.Memory.Limit) + assert.Equal(t, memLimit, *spec.Linux.Resources.Memory.Limit) +} + +func TestUpdateContainerResources(t *testing.T) { + t.Log("Create a sandbox") + sbConfig := PodSandboxConfig("sandbox", "update-container-resources") + sb, err := runtimeService.RunPodSandbox(sbConfig, *runtimeHandler) + require.NoError(t, err) + defer func() { + assert.NoError(t, runtimeService.StopPodSandbox(sb)) + assert.NoError(t, runtimeService.RemovePodSandbox(sb)) + }() + + t.Log("Create a container with memory limit") + cnConfig := ContainerConfig( + "container", + pauseImage, + WithResources(&runtime.LinuxContainerResources{ + MemoryLimitInBytes: 200 * 1024 * 1024, + }), + ) + cn, err := runtimeService.CreateContainer(sb, cnConfig, sbConfig) + require.NoError(t, err) + + t.Log("Check memory limit in container OCI spec") + container, err := containerdClient.LoadContainer(context.Background(), cn) + require.NoError(t, err) + spec, err := container.Spec(context.Background()) + require.NoError(t, err) + checkMemoryLimit(t, spec, 200*1024*1024) + + t.Log("Update container memory limit after created") + err = runtimeService.UpdateContainerResources(cn, &runtime.LinuxContainerResources{ + MemoryLimitInBytes: 400 * 1024 * 1024, + }) + require.NoError(t, err) + + t.Log("Check memory limit in container OCI spec") + spec, err = container.Spec(context.Background()) + require.NoError(t, err) + checkMemoryLimit(t, spec, 400*1024*1024) + + t.Log("Start the container") + require.NoError(t, runtimeService.StartContainer(cn)) + task, err := container.Task(context.Background(), nil) + require.NoError(t, err) + + t.Log("Check memory limit in cgroup") + cgroup, err := cgroups.Load(cgroups.V1, cgroups.PidPath(int(task.Pid()))) + require.NoError(t, err) + stat, err := cgroup.Stat(cgroups.IgnoreNotExist) + require.NoError(t, err) + assert.Equal(t, uint64(400*1024*1024), stat.Memory.Usage.Limit) + + t.Log("Update container memory limit after started") + err = runtimeService.UpdateContainerResources(cn, &runtime.LinuxContainerResources{ + MemoryLimitInBytes: 800 * 1024 * 1024, + }) + require.NoError(t, err) + + t.Log("Check memory limit in container OCI spec") + spec, err = container.Spec(context.Background()) + require.NoError(t, err) + checkMemoryLimit(t, spec, 800*1024*1024) + + t.Log("Check memory limit in cgroup") + stat, err = cgroup.Stat(cgroups.IgnoreNotExist) + require.NoError(t, err) + assert.Equal(t, uint64(800*1024*1024), stat.Memory.Usage.Limit) +} diff --git a/integration/container_without_image_ref_test.go b/integration/container_without_image_ref_test.go new file mode 100644 index 000000000..9fc897789 --- /dev/null +++ b/integration/container_without_image_ref_test.go @@ -0,0 +1,77 @@ +// +build linux + +/* + Copyright The containerd 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 + + http://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. +*/ + +package integration + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" +) + +// Test container lifecycle can work without image references. +func TestContainerLifecycleWithoutImageRef(t *testing.T) { + t.Log("Create a sandbox") + sbConfig := PodSandboxConfig("sandbox", "container-lifecycle-without-image-ref") + sb, err := runtimeService.RunPodSandbox(sbConfig, *runtimeHandler) + require.NoError(t, err) + defer func() { + assert.NoError(t, runtimeService.StopPodSandbox(sb)) + assert.NoError(t, runtimeService.RemovePodSandbox(sb)) + }() + + const ( + testImage = "busybox" + containerName = "test-container" + ) + t.Log("Pull test image") + img, err := imageService.PullImage(&runtime.ImageSpec{Image: testImage}, nil, sbConfig) + require.NoError(t, err) + defer func() { + assert.NoError(t, imageService.RemoveImage(&runtime.ImageSpec{Image: img})) + }() + + t.Log("Create test container") + cnConfig := ContainerConfig( + containerName, + testImage, + WithCommand("sleep", "1000"), + ) + cn, err := runtimeService.CreateContainer(sb, cnConfig, sbConfig) + require.NoError(t, err) + require.NoError(t, runtimeService.StartContainer(cn)) + + t.Log("Remove test image") + assert.NoError(t, imageService.RemoveImage(&runtime.ImageSpec{Image: img})) + + t.Log("Container status should be running") + status, err := runtimeService.ContainerStatus(cn) + require.NoError(t, err) + assert.Equal(t, status.GetState(), runtime.ContainerState_CONTAINER_RUNNING) + + t.Logf("Stop container") + err = runtimeService.StopContainer(cn, 1) + assert.NoError(t, err) + + t.Log("Container status should be exited") + status, err = runtimeService.ContainerStatus(cn) + require.NoError(t, err) + assert.Equal(t, status.GetState(), runtime.ContainerState_CONTAINER_EXITED) +} diff --git a/integration/containerd_image_test.go b/integration/containerd_image_test.go new file mode 100644 index 000000000..1832b50ff --- /dev/null +++ b/integration/containerd_image_test.go @@ -0,0 +1,213 @@ +// +build linux + +/* + Copyright The containerd 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 + + http://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. +*/ + +package integration + +import ( + "testing" + "time" + + "golang.org/x/net/context" + + "github.com/containerd/containerd" + "github.com/containerd/containerd/errdefs" + "github.com/containerd/containerd/namespaces" + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" +) + +// Test to test the CRI plugin should see image pulled into containerd directly. +func TestContainerdImage(t *testing.T) { + const testImage = "docker.io/library/busybox:latest" + ctx := context.Background() + + t.Logf("make sure the test image doesn't exist in the cri plugin") + i, err := imageService.ImageStatus(&runtime.ImageSpec{Image: testImage}) + require.NoError(t, err) + if i != nil { + require.NoError(t, imageService.RemoveImage(&runtime.ImageSpec{Image: testImage})) + } + + t.Logf("pull the image into containerd") + _, err = containerdClient.Pull(ctx, testImage, containerd.WithPullUnpack) + assert.NoError(t, err) + defer func() { + // Make sure the image is cleaned up in any case. + if err := containerdClient.ImageService().Delete(ctx, testImage); err != nil { + assert.True(t, errdefs.IsNotFound(err), err) + } + assert.NoError(t, imageService.RemoveImage(&runtime.ImageSpec{Image: testImage})) + }() + + t.Logf("the image should be seen by the cri plugin") + var id string + checkImage := func() (bool, error) { + img, err := imageService.ImageStatus(&runtime.ImageSpec{Image: testImage}) + if err != nil { + return false, err + } + if img == nil { + t.Logf("Image %q not show up in the cri plugin yet", testImage) + return false, nil + } + id = img.Id + img, err = imageService.ImageStatus(&runtime.ImageSpec{Image: id}) + if err != nil { + return false, err + } + if img == nil { + // We always generate image id as a reference first, it must + // be ready here. + return false, errors.New("can't reference image by id") + } + if len(img.RepoTags) != 1 { + // RepoTags must have been populated correctly. + return false, errors.Errorf("unexpected repotags: %+v", img.RepoTags) + } + if img.RepoTags[0] != testImage { + return false, errors.Errorf("unexpected repotag %q", img.RepoTags[0]) + } + return true, nil + } + require.NoError(t, Eventually(checkImage, 100*time.Millisecond, 10*time.Second)) + require.NoError(t, Consistently(checkImage, 100*time.Millisecond, time.Second)) + defer func() { + t.Logf("image should still be seen by id if only tag get deleted") + if err := containerdClient.ImageService().Delete(ctx, testImage); err != nil { + assert.True(t, errdefs.IsNotFound(err), err) + } + assert.NoError(t, Consistently(func() (bool, error) { + img, err := imageService.ImageStatus(&runtime.ImageSpec{Image: id}) + if err != nil { + return false, err + } + return img != nil, nil + }, 100*time.Millisecond, time.Second)) + t.Logf("image should be removed from the cri plugin if all references get deleted") + if err := containerdClient.ImageService().Delete(ctx, id); err != nil { + assert.True(t, errdefs.IsNotFound(err), err) + } + assert.NoError(t, Eventually(func() (bool, error) { + img, err := imageService.ImageStatus(&runtime.ImageSpec{Image: id}) + if err != nil { + return false, err + } + return img == nil, nil + }, 100*time.Millisecond, 10*time.Second)) + }() + + t.Logf("the image should be marked as managed") + imgByRef, err := containerdClient.GetImage(ctx, testImage) + assert.NoError(t, err) + assert.Equal(t, imgByRef.Labels()["io.cri-containerd.image"], "managed") + + t.Logf("the image id should be created and managed") + imgByID, err := containerdClient.GetImage(ctx, id) + assert.NoError(t, err) + assert.Equal(t, imgByID.Labels()["io.cri-containerd.image"], "managed") + + t.Logf("should be able to start container with the image") + sbConfig := PodSandboxConfig("sandbox", "containerd-image") + sb, err := runtimeService.RunPodSandbox(sbConfig, *runtimeHandler) + require.NoError(t, err) + defer func() { + assert.NoError(t, runtimeService.StopPodSandbox(sb)) + assert.NoError(t, runtimeService.RemovePodSandbox(sb)) + }() + + cnConfig := ContainerConfig( + "test-container", + id, + WithCommand("top"), + ) + cn, err := runtimeService.CreateContainer(sb, cnConfig, sbConfig) + require.NoError(t, err) + require.NoError(t, runtimeService.StartContainer(cn)) + checkContainer := func() (bool, error) { + s, err := runtimeService.ContainerStatus(cn) + if err != nil { + return false, err + } + return s.GetState() == runtime.ContainerState_CONTAINER_RUNNING, nil + } + require.NoError(t, Eventually(checkContainer, 100*time.Millisecond, 10*time.Second)) + require.NoError(t, Consistently(checkContainer, 100*time.Millisecond, time.Second)) +} + +// Test image managed by CRI plugin shouldn't be affected by images in other namespaces. +func TestContainerdImageInOtherNamespaces(t *testing.T) { + const testImage = "docker.io/library/busybox:latest" + ctx := context.Background() + + t.Logf("make sure the test image doesn't exist in the cri plugin") + i, err := imageService.ImageStatus(&runtime.ImageSpec{Image: testImage}) + require.NoError(t, err) + if i != nil { + require.NoError(t, imageService.RemoveImage(&runtime.ImageSpec{Image: testImage})) + } + + t.Logf("pull the image into test namespace") + namespacedCtx := namespaces.WithNamespace(ctx, "test") + _, err = containerdClient.Pull(namespacedCtx, testImage, containerd.WithPullUnpack) + assert.NoError(t, err) + defer func() { + // Make sure the image is cleaned up in any case. + if err := containerdClient.ImageService().Delete(namespacedCtx, testImage); err != nil { + assert.True(t, errdefs.IsNotFound(err), err) + } + assert.NoError(t, imageService.RemoveImage(&runtime.ImageSpec{Image: testImage})) + }() + + t.Logf("cri plugin should not see the image") + checkImage := func() (bool, error) { + img, err := imageService.ImageStatus(&runtime.ImageSpec{Image: testImage}) + if err != nil { + return false, err + } + return img == nil, nil + } + require.NoError(t, Consistently(checkImage, 100*time.Millisecond, time.Second)) + + sbConfig := PodSandboxConfig("sandbox", "test") + t.Logf("pull the image into cri plugin") + id, err := imageService.PullImage(&runtime.ImageSpec{Image: testImage}, nil, sbConfig) + require.NoError(t, err) + defer func() { + assert.NoError(t, imageService.RemoveImage(&runtime.ImageSpec{Image: id})) + }() + + t.Logf("cri plugin should see the image now") + img, err := imageService.ImageStatus(&runtime.ImageSpec{Image: testImage}) + require.NoError(t, err) + assert.NotNil(t, img) + + t.Logf("remove the image from test namespace") + require.NoError(t, containerdClient.ImageService().Delete(namespacedCtx, testImage)) + + t.Logf("cri plugin should still see the image") + checkImage = func() (bool, error) { + img, err := imageService.ImageStatus(&runtime.ImageSpec{Image: testImage}) + if err != nil { + return false, err + } + return img != nil, nil + } + assert.NoError(t, Consistently(checkImage, 100*time.Millisecond, time.Second)) +} diff --git a/integration/duplicate_name_test.go b/integration/duplicate_name_test.go new file mode 100644 index 000000000..6c8532063 --- /dev/null +++ b/integration/duplicate_name_test.go @@ -0,0 +1,53 @@ +// +build linux + +/* + Copyright The containerd 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 + + http://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. +*/ + +package integration + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestDuplicateName(t *testing.T) { + t.Logf("Create a sandbox") + sbConfig := PodSandboxConfig("sandbox", "duplicate-name") + sb, err := runtimeService.RunPodSandbox(sbConfig, *runtimeHandler) + require.NoError(t, err) + defer func() { + assert.NoError(t, runtimeService.StopPodSandbox(sb)) + assert.NoError(t, runtimeService.RemovePodSandbox(sb)) + }() + + t.Logf("Create the sandbox again should fail") + _, err = runtimeService.RunPodSandbox(sbConfig, *runtimeHandler) + require.Error(t, err) + + t.Logf("Create a container") + cnConfig := ContainerConfig( + "container", + pauseImage, + ) + _, err = runtimeService.CreateContainer(sb, cnConfig, sbConfig) + require.NoError(t, err) + + t.Logf("Create the container again should fail") + _, err = runtimeService.CreateContainer(sb, cnConfig, sbConfig) + require.Error(t, err) +} diff --git a/integration/image_load_test.go b/integration/image_load_test.go new file mode 100644 index 000000000..ab7fed613 --- /dev/null +++ b/integration/image_load_test.go @@ -0,0 +1,101 @@ +// +build linux + +/* + Copyright The containerd 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 + + http://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. +*/ + +package integration + +import ( + "io/ioutil" + "os" + "os/exec" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" +) + +// Test to load an image from tarball. +func TestImageLoad(t *testing.T) { + testImage := "busybox:latest" + loadedImage := "docker.io/library/" + testImage + _, err := exec.LookPath("docker") + if err != nil { + t.Skipf("Docker is not available: %v", err) + } + t.Logf("docker save image into tarball") + output, err := exec.Command("docker", "pull", testImage).CombinedOutput() + require.NoError(t, err, "output: %q", output) + tarF, err := ioutil.TempFile("", "image-load") + tar := tarF.Name() + require.NoError(t, err) + defer func() { + assert.NoError(t, os.RemoveAll(tar)) + }() + output, err = exec.Command("docker", "save", testImage, "-o", tar).CombinedOutput() + require.NoError(t, err, "output: %q", output) + + t.Logf("make sure no such image in cri") + img, err := imageService.ImageStatus(&runtime.ImageSpec{Image: testImage}) + require.NoError(t, err) + if img != nil { + require.NoError(t, imageService.RemoveImage(&runtime.ImageSpec{Image: testImage})) + } + + t.Logf("load image in cri") + ctr, err := exec.LookPath("ctr") + require.NoError(t, err, "ctr should be installed, make sure you've run `make install.deps`") + output, err = exec.Command(ctr, "-address="+containerdEndpoint, + "-n=k8s.io", "images", "import", tar).CombinedOutput() + require.NoError(t, err, "output: %q", output) + + t.Logf("make sure image is loaded") + // Use Eventually because the cri plugin needs a short period of time + // to pick up images imported into containerd directly. + require.NoError(t, Eventually(func() (bool, error) { + img, err = imageService.ImageStatus(&runtime.ImageSpec{Image: testImage}) + if err != nil { + return false, err + } + return img != nil, nil + }, 100*time.Millisecond, 10*time.Second)) + require.Equal(t, []string{loadedImage}, img.RepoTags) + + t.Logf("create a container with the loaded image") + sbConfig := PodSandboxConfig("sandbox", Randomize("image-load")) + sb, err := runtimeService.RunPodSandbox(sbConfig, *runtimeHandler) + require.NoError(t, err) + defer func() { + assert.NoError(t, runtimeService.StopPodSandbox(sb)) + assert.NoError(t, runtimeService.RemovePodSandbox(sb)) + }() + containerConfig := ContainerConfig( + "container", + testImage, + WithCommand("tail", "-f", "/dev/null"), + ) + // Rely on sandbox clean to do container cleanup. + cn, err := runtimeService.CreateContainer(sb, containerConfig, sbConfig) + require.NoError(t, err) + require.NoError(t, runtimeService.StartContainer(cn)) + + t.Logf("make sure container is running") + status, err := runtimeService.ContainerStatus(cn) + require.NoError(t, err) + require.Equal(t, runtime.ContainerState_CONTAINER_RUNNING, status.State) +} diff --git a/integration/imagefs_info_test.go b/integration/imagefs_info_test.go new file mode 100644 index 000000000..1fc01e9ea --- /dev/null +++ b/integration/imagefs_info_test.go @@ -0,0 +1,78 @@ +// +build linux + +/* + Copyright The containerd 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 + + http://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. +*/ + +package integration + +import ( + "os" + "testing" + "time" + + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" +) + +func TestImageFSInfo(t *testing.T) { + config := PodSandboxConfig("running-pod", "imagefs") + + t.Logf("Pull an image to make sure image fs is not empty") + img, err := imageService.PullImage(&runtime.ImageSpec{Image: "busybox"}, nil, config) + require.NoError(t, err) + defer func() { + err := imageService.RemoveImage(&runtime.ImageSpec{Image: img}) + assert.NoError(t, err) + }() + t.Logf("Create a sandbox to make sure there is an active snapshot") + sb, err := runtimeService.RunPodSandbox(config, *runtimeHandler) + require.NoError(t, err) + defer func() { + assert.NoError(t, runtimeService.StopPodSandbox(sb)) + assert.NoError(t, runtimeService.RemovePodSandbox(sb)) + }() + + // It takes time to populate imagefs stats. Use eventually + // to check for a period of time. + t.Logf("Check imagefs info") + var info *runtime.FilesystemUsage + require.NoError(t, Eventually(func() (bool, error) { + stats, err := imageService.ImageFsInfo() + if err != nil { + return false, err + } + if len(stats) == 0 { + return false, nil + } + if len(stats) >= 2 { + return false, errors.Errorf("unexpected stats length: %d", len(stats)) + } + info = stats[0] + if info.GetTimestamp() != 0 && + info.GetUsedBytes().GetValue() != 0 && + info.GetInodesUsed().GetValue() != 0 && + info.GetFsId().GetMountpoint() != "" { + return true, nil + } + return false, nil + }, time.Second, 30*time.Second)) + + t.Logf("Image filesystem mountpath should exist") + _, err = os.Stat(info.GetFsId().GetMountpoint()) + assert.NoError(t, err) +} diff --git a/integration/images/volume-copy-up/Dockerfile b/integration/images/volume-copy-up/Dockerfile new file mode 100644 index 000000000..ed6bba63b --- /dev/null +++ b/integration/images/volume-copy-up/Dockerfile @@ -0,0 +1,17 @@ +# Copyright The containerd 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 + +# http://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. + +FROM busybox +RUN sh -c "mkdir /test_dir; echo test_content > /test_dir/test_file" +VOLUME "/test_dir" diff --git a/integration/images/volume-copy-up/Makefile b/integration/images/volume-copy-up/Makefile new file mode 100644 index 000000000..f5721c4a9 --- /dev/null +++ b/integration/images/volume-copy-up/Makefile @@ -0,0 +1,27 @@ +# Copyright The containerd 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 + +# http://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. + +all: build + +PROJ=gcr.io/k8s-cri-containerd +VERSION=1.0 +IMAGE=$(PROJ)/volume-copy-up:$(VERSION) + +build: + docker build -t $(IMAGE) . + +push: + gcloud docker -- push $(IMAGE) + +.PHONY: build push diff --git a/integration/images/volume-ownership/Dockerfile b/integration/images/volume-ownership/Dockerfile new file mode 100644 index 000000000..f7a9086b3 --- /dev/null +++ b/integration/images/volume-ownership/Dockerfile @@ -0,0 +1,18 @@ +# Copyright The containerd 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 + +# http://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. + +FROM busybox +RUN mkdir -p /test_dir && \ + chown -R nobody:nogroup /test_dir +VOLUME /test_dir diff --git a/integration/images/volume-ownership/Makefile b/integration/images/volume-ownership/Makefile new file mode 100644 index 000000000..d4654d292 --- /dev/null +++ b/integration/images/volume-ownership/Makefile @@ -0,0 +1,27 @@ +# Copyright The containerd 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 + +# http://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. + +all: build + +PROJ=gcr.io/k8s-cri-containerd +VERSION=1.0 +IMAGE=$(PROJ)/volume-ownership:$(VERSION) + +build: + docker build -t $(IMAGE) . + +push: + gcloud docker -- push $(IMAGE) + +.PHONY: build push diff --git a/integration/main_test.go b/integration/main_test.go new file mode 100644 index 000000000..95480c382 --- /dev/null +++ b/integration/main_test.go @@ -0,0 +1,418 @@ +// +build linux + +/* + Copyright The containerd 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 + + http://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. +*/ + +package integration + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "os" + "os/exec" + "strconv" + "strings" + "testing" + "time" + + "github.com/containerd/containerd" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + cri "k8s.io/cri-api/pkg/apis" + runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" + + "github.com/containerd/containerd/integration/remote" + dialer "github.com/containerd/containerd/integration/util" + criconfig "github.com/containerd/containerd/pkg/config" + "github.com/containerd/containerd/pkg/constants" + "github.com/containerd/containerd/pkg/server" + "github.com/containerd/containerd/pkg/util" +) + +const ( + timeout = 1 * time.Minute + pauseImage = "k8s.gcr.io/pause:3.2" // This is the same with default sandbox image. + k8sNamespace = constants.K8sContainerdNamespace +) + +var ( + runtimeService cri.RuntimeService + imageService cri.ImageManagerService + containerdClient *containerd.Client + containerdEndpoint string +) + +var criEndpoint = flag.String("cri-endpoint", "unix:///run/containerd/containerd.sock", "The endpoint of cri plugin.") +var criRoot = flag.String("cri-root", "/var/lib/containerd/io.containerd.grpc.v1.cri", "The root directory of cri plugin.") +var runtimeHandler = flag.String("runtime-handler", "", "The runtime handler to use in the test.") +var containerdBin = flag.String("containerd-bin", "containerd", "The containerd binary name. The name is used to restart containerd during test.") + +func TestMain(m *testing.M) { + flag.Parse() + if err := ConnectDaemons(); err != nil { + logrus.WithError(err).Fatalf("Failed to connect daemons") + } + os.Exit(m.Run()) +} + +// ConnectDaemons connect cri plugin and containerd, and initialize the clients. +func ConnectDaemons() error { + var err error + runtimeService, err = remote.NewRuntimeService(*criEndpoint, timeout) + if err != nil { + return errors.Wrap(err, "failed to create runtime service") + } + imageService, err = remote.NewImageService(*criEndpoint, timeout) + if err != nil { + return errors.Wrap(err, "failed to create image service") + } + // Since CRI grpc client doesn't have `WithBlock` specified, we + // need to check whether it is actually connected. + // TODO(random-liu): Extend cri remote client to accept extra grpc options. + _, err = runtimeService.ListContainers(&runtime.ContainerFilter{}) + if err != nil { + return errors.Wrap(err, "failed to list containers") + } + _, err = imageService.ListImages(&runtime.ImageFilter{}) + if err != nil { + return errors.Wrap(err, "failed to list images") + } + // containerdEndpoint is the same with criEndpoint now + containerdEndpoint = strings.TrimPrefix(*criEndpoint, "unix://") + containerdClient, err = containerd.New(containerdEndpoint, containerd.WithDefaultNamespace(k8sNamespace)) + if err != nil { + return errors.Wrap(err, "failed to connect containerd") + } + return nil +} + +// Opts sets specific information in pod sandbox config. +type PodSandboxOpts func(*runtime.PodSandboxConfig) + +// Set host network. +func WithHostNetwork(p *runtime.PodSandboxConfig) { + if p.Linux == nil { + p.Linux = &runtime.LinuxPodSandboxConfig{} + } + if p.Linux.SecurityContext == nil { + p.Linux.SecurityContext = &runtime.LinuxSandboxSecurityContext{} + } + if p.Linux.SecurityContext.NamespaceOptions == nil { + p.Linux.SecurityContext.NamespaceOptions = &runtime.NamespaceOption{} + } + p.Linux.SecurityContext.NamespaceOptions.Network = runtime.NamespaceMode_NODE +} + +// Set host pid. +func WithHostPid(p *runtime.PodSandboxConfig) { + if p.Linux == nil { + p.Linux = &runtime.LinuxPodSandboxConfig{} + } + if p.Linux.SecurityContext == nil { + p.Linux.SecurityContext = &runtime.LinuxSandboxSecurityContext{} + } + if p.Linux.SecurityContext.NamespaceOptions == nil { + p.Linux.SecurityContext.NamespaceOptions = &runtime.NamespaceOption{} + } + p.Linux.SecurityContext.NamespaceOptions.Pid = runtime.NamespaceMode_NODE +} + +// Set pod pid. +func WithPodPid(p *runtime.PodSandboxConfig) { + if p.Linux == nil { + p.Linux = &runtime.LinuxPodSandboxConfig{} + } + if p.Linux.SecurityContext == nil { + p.Linux.SecurityContext = &runtime.LinuxSandboxSecurityContext{} + } + if p.Linux.SecurityContext.NamespaceOptions == nil { + p.Linux.SecurityContext.NamespaceOptions = &runtime.NamespaceOption{} + } + p.Linux.SecurityContext.NamespaceOptions.Pid = runtime.NamespaceMode_POD +} + +// Add pod log directory. +func WithPodLogDirectory(dir string) PodSandboxOpts { + return func(p *runtime.PodSandboxConfig) { + p.LogDirectory = dir + } +} + +// Add pod hostname. +func WithPodHostname(hostname string) PodSandboxOpts { + return func(p *runtime.PodSandboxConfig) { + p.Hostname = hostname + } +} + +// PodSandboxConfig generates a pod sandbox config for test. +func PodSandboxConfig(name, ns string, opts ...PodSandboxOpts) *runtime.PodSandboxConfig { + config := &runtime.PodSandboxConfig{ + Metadata: &runtime.PodSandboxMetadata{ + Name: name, + // Using random id as uuid is good enough for local + // integration test. + Uid: util.GenerateID(), + Namespace: Randomize(ns), + }, + Linux: &runtime.LinuxPodSandboxConfig{}, + } + for _, opt := range opts { + opt(config) + } + return config +} + +// ContainerOpts to set any specific attribute like labels, +// annotations, metadata etc +type ContainerOpts func(*runtime.ContainerConfig) + +func WithTestLabels() ContainerOpts { + return func(c *runtime.ContainerConfig) { + c.Labels = map[string]string{"key": "value"} + } +} + +func WithTestAnnotations() ContainerOpts { + return func(c *runtime.ContainerConfig) { + c.Annotations = map[string]string{"a.b.c": "test"} + } +} + +// Add container resource limits. +func WithResources(r *runtime.LinuxContainerResources) ContainerOpts { + return func(c *runtime.ContainerConfig) { + if c.Linux == nil { + c.Linux = &runtime.LinuxContainerConfig{} + } + c.Linux.Resources = r + } +} + +// Add container command. +func WithCommand(cmd string, args ...string) ContainerOpts { + return func(c *runtime.ContainerConfig) { + c.Command = []string{cmd} + c.Args = args + } +} + +// Add pid namespace mode. +func WithPidNamespace(mode runtime.NamespaceMode) ContainerOpts { + return func(c *runtime.ContainerConfig) { + if c.Linux == nil { + c.Linux = &runtime.LinuxContainerConfig{} + } + if c.Linux.SecurityContext == nil { + c.Linux.SecurityContext = &runtime.LinuxContainerSecurityContext{} + } + if c.Linux.SecurityContext.NamespaceOptions == nil { + c.Linux.SecurityContext.NamespaceOptions = &runtime.NamespaceOption{} + } + c.Linux.SecurityContext.NamespaceOptions.Pid = mode + } + +} + +// Add container log path. +func WithLogPath(path string) ContainerOpts { + return func(c *runtime.ContainerConfig) { + c.LogPath = path + } +} + +// WithSupplementalGroups adds supplemental groups. +func WithSupplementalGroups(gids []int64) ContainerOpts { + return func(c *runtime.ContainerConfig) { + if c.Linux == nil { + c.Linux = &runtime.LinuxContainerConfig{} + } + if c.Linux.SecurityContext == nil { + c.Linux.SecurityContext = &runtime.LinuxContainerSecurityContext{} + } + c.Linux.SecurityContext.SupplementalGroups = gids + } +} + +// ContainerConfig creates a container config given a name and image name +// and additional container config options +func ContainerConfig(name, image string, opts ...ContainerOpts) *runtime.ContainerConfig { + cConfig := &runtime.ContainerConfig{ + Metadata: &runtime.ContainerMetadata{ + Name: name, + }, + Image: &runtime.ImageSpec{Image: image}, + } + for _, opt := range opts { + opt(cConfig) + } + return cConfig +} + +// CheckFunc is the function used to check a condition is true/false. +type CheckFunc func() (bool, error) + +// Eventually waits for f to return true, it checks every period, and +// returns error if timeout exceeds. If f returns error, Eventually +// will return the same error immediately. +func Eventually(f CheckFunc, period, timeout time.Duration) error { + start := time.Now() + for { + done, err := f() + if done { + return nil + } + if err != nil { + return err + } + if time.Since(start) >= timeout { + return errors.New("timeout exceeded") + } + time.Sleep(period) + } +} + +// Consistently makes sure that f consistently returns true without +// error before timeout exceeds. If f returns error, Consistently +// will return the same error immediately. +func Consistently(f CheckFunc, period, timeout time.Duration) error { + start := time.Now() + for { + ok, err := f() + if !ok { + return errors.New("get false") + } + if err != nil { + return err + } + if time.Since(start) >= timeout { + return nil + } + time.Sleep(period) + } +} + +// Randomize adds uuid after a string. +func Randomize(str string) string { + return str + "-" + util.GenerateID() +} + +// KillProcess kills the process by name. pkill is used. +func KillProcess(name string) error { + output, err := exec.Command("pkill", "-x", fmt.Sprintf("^%s$", name)).CombinedOutput() + if err != nil { + return errors.Errorf("failed to kill %q - error: %v, output: %q", name, err, output) + } + return nil +} + +// KillPid kills the process by pid. kill is used. +func KillPid(pid int) error { + output, err := exec.Command("kill", strconv.Itoa(pid)).CombinedOutput() + if err != nil { + return errors.Errorf("failed to kill %d - error: %v, output: %q", pid, err, output) + } + return nil +} + +// PidOf returns pid of a process by name. +func PidOf(name string) (int, error) { + b, err := exec.Command("pidof", name).CombinedOutput() + output := strings.TrimSpace(string(b)) + if err != nil { + if len(output) != 0 { + return 0, errors.Errorf("failed to run pidof %q - error: %v, output: %q", name, err, output) + } + return 0, nil + } + return strconv.Atoi(output) +} + +// RawRuntimeClient returns a raw grpc runtime service client. +func RawRuntimeClient() (runtime.RuntimeServiceClient, error) { + addr, dialer, err := dialer.GetAddressAndDialer(*criEndpoint) + if err != nil { + return nil, errors.Wrap(err, "failed to get dialer") + } + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + conn, err := grpc.DialContext(ctx, addr, grpc.WithInsecure(), grpc.WithContextDialer(dialer)) + if err != nil { + return nil, errors.Wrap(err, "failed to connect cri endpoint") + } + return runtime.NewRuntimeServiceClient(conn), nil +} + +// CRIConfig gets current cri config from containerd. +func CRIConfig() (*criconfig.Config, error) { + client, err := RawRuntimeClient() + if err != nil { + return nil, errors.Wrap(err, "failed to get raw runtime client") + } + resp, err := client.Status(context.Background(), &runtime.StatusRequest{Verbose: true}) + if err != nil { + return nil, errors.Wrap(err, "failed to get status") + } + config := &criconfig.Config{} + if err := json.Unmarshal([]byte(resp.Info["config"]), config); err != nil { + return nil, errors.Wrap(err, "failed to unmarshal config") + } + return config, nil +} + +// SandboxInfo gets sandbox info. +func SandboxInfo(id string) (*runtime.PodSandboxStatus, *server.SandboxInfo, error) { + client, err := RawRuntimeClient() + if err != nil { + return nil, nil, errors.Wrap(err, "failed to get raw runtime client") + } + resp, err := client.PodSandboxStatus(context.Background(), &runtime.PodSandboxStatusRequest{ + PodSandboxId: id, + Verbose: true, + }) + if err != nil { + return nil, nil, errors.Wrap(err, "failed to get sandbox status") + } + status := resp.GetStatus() + var info server.SandboxInfo + if err := json.Unmarshal([]byte(resp.GetInfo()["info"]), &info); err != nil { + return nil, nil, errors.Wrap(err, "failed to unmarshal sandbox info") + } + return status, &info, nil +} + +func RestartContainerd(t *testing.T) { + require.NoError(t, KillProcess(*containerdBin)) + + // Use assert so that the 3rd wait always runs, this makes sure + // containerd is running before this function returns. + assert.NoError(t, Eventually(func() (bool, error) { + pid, err := PidOf(*containerdBin) + if err != nil { + return false, err + } + return pid == 0, nil + }, time.Second, 30*time.Second), "wait for containerd to be killed") + + require.NoError(t, Eventually(func() (bool, error) { + return ConnectDaemons() == nil, nil + }, time.Second, 30*time.Second), "wait for containerd to be restarted") +} diff --git a/integration/no_metadata_test.go b/integration/no_metadata_test.go new file mode 100644 index 000000000..5766e19b4 --- /dev/null +++ b/integration/no_metadata_test.go @@ -0,0 +1,50 @@ +// +build linux + +/* + Copyright The containerd 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 + + http://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. +*/ + +package integration + +import ( + "testing" + + "github.com/stretchr/testify/require" + runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" +) + +func TestRunPodSandboxWithoutMetadata(t *testing.T) { + sbConfig := &runtime.PodSandboxConfig{} + _, err := runtimeService.RunPodSandbox(sbConfig, *runtimeHandler) + require.Error(t, err) + _, err = runtimeService.Status() + require.NoError(t, err) +} + +func TestCreateContainerWithoutMetadata(t *testing.T) { + sbConfig := PodSandboxConfig("sandbox", "container-create") + sb, err := runtimeService.RunPodSandbox(sbConfig, *runtimeHandler) + require.NoError(t, err) + defer func() { + // Make sure the sandbox is cleaned up in any case. + runtimeService.StopPodSandbox(sb) + runtimeService.RemovePodSandbox(sb) + }() + config := &runtime.ContainerConfig{} + _, err = runtimeService.CreateContainer(sb, config, sbConfig) + require.Error(t, err) + _, err = runtimeService.Status() + require.NoError(t, err) +} diff --git a/integration/pod_dualstack_test.go b/integration/pod_dualstack_test.go new file mode 100644 index 000000000..acaacdd56 --- /dev/null +++ b/integration/pod_dualstack_test.go @@ -0,0 +1,107 @@ +// +build linux + +/* + Copyright The containerd 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 + + http://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. +*/ + +package integration + +import ( + "io/ioutil" + "net" + "os" + "path/filepath" + "regexp" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" +) + +func TestPodDualStack(t *testing.T) { + testPodLogDir, err := ioutil.TempDir("/tmp", "dualstack") + require.NoError(t, err) + defer os.RemoveAll(testPodLogDir) + + t.Log("Create a sandbox") + sbConfig := PodSandboxConfig("sandbox", "dualstack", WithPodLogDirectory(testPodLogDir)) + sb, err := runtimeService.RunPodSandbox(sbConfig, *runtimeHandler) + require.NoError(t, err) + defer func() { + assert.NoError(t, runtimeService.StopPodSandbox(sb)) + assert.NoError(t, runtimeService.RemovePodSandbox(sb)) + }() + + const ( + testImage = "busybox" + containerName = "test-container" + ) + t.Logf("Pull test image %q", testImage) + img, err := imageService.PullImage(&runtime.ImageSpec{Image: testImage}, nil, sbConfig) + require.NoError(t, err) + defer func() { + assert.NoError(t, imageService.RemoveImage(&runtime.ImageSpec{Image: img})) + }() + + t.Log("Create a container to print env") + cnConfig := ContainerConfig( + containerName, + testImage, + WithCommand("ip", "address", "show", "dev", "eth0"), + WithLogPath(containerName), + ) + cn, err := runtimeService.CreateContainer(sb, cnConfig, sbConfig) + require.NoError(t, err) + + t.Log("Start the container") + require.NoError(t, runtimeService.StartContainer(cn)) + + t.Log("Wait for container to finish running") + require.NoError(t, Eventually(func() (bool, error) { + s, err := runtimeService.ContainerStatus(cn) + if err != nil { + return false, err + } + if s.GetState() == runtime.ContainerState_CONTAINER_EXITED { + return true, nil + } + return false, nil + }, time.Second, 30*time.Second)) + + content, err := ioutil.ReadFile(filepath.Join(testPodLogDir, containerName)) + assert.NoError(t, err) + status, err := runtimeService.PodSandboxStatus(sb) + require.NoError(t, err) + ip := status.GetNetwork().GetIp() + additionalIps := status.GetNetwork().GetAdditionalIps() + + ipv4Enabled, err := regexp.MatchString("inet .* scope global", string(content)) + assert.NoError(t, err) + ipv6Enabled, err := regexp.MatchString("inet6 .* scope global", string(content)) + assert.NoError(t, err) + + if ipv4Enabled && ipv6Enabled { + t.Log("Dualstack should be enabled") + require.Len(t, additionalIps, 1) + assert.NotNil(t, net.ParseIP(ip).To4()) + assert.Nil(t, net.ParseIP(additionalIps[0].GetIp()).To4()) + } else { + t.Log("Dualstack should not be enabled") + assert.Len(t, additionalIps, 0) + assert.NotEmpty(t, ip) + } +} diff --git a/integration/pod_hostname_test.go b/integration/pod_hostname_test.go new file mode 100644 index 000000000..5b87f6dc8 --- /dev/null +++ b/integration/pod_hostname_test.go @@ -0,0 +1,132 @@ +// +build linux + +/* + Copyright The containerd 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 + + http://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. +*/ + +package integration + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" +) + +func TestPodHostname(t *testing.T) { + hostname, err := os.Hostname() + require.NoError(t, err) + for name, test := range map[string]struct { + opts []PodSandboxOpts + expectedHostname string + expectErr bool + }{ + "regular pod with custom hostname": { + opts: []PodSandboxOpts{ + WithPodHostname("test-hostname"), + }, + expectedHostname: "test-hostname", + }, + "host network pod without custom hostname": { + opts: []PodSandboxOpts{ + WithHostNetwork, + }, + expectedHostname: hostname, + }, + "host network pod with custom hostname should fail": { + opts: []PodSandboxOpts{ + WithHostNetwork, + WithPodHostname("test-hostname"), + }, + expectErr: true, + }, + } { + t.Run(name, func(t *testing.T) { + testPodLogDir, err := ioutil.TempDir("/tmp", "hostname") + require.NoError(t, err) + defer os.RemoveAll(testPodLogDir) + + opts := append(test.opts, WithPodLogDirectory(testPodLogDir)) + t.Log("Create a sandbox with hostname") + sbConfig := PodSandboxConfig("sandbox", "hostname", opts...) + sb, err := runtimeService.RunPodSandbox(sbConfig, *runtimeHandler) + if err != nil { + if !test.expectErr { + t.Fatalf("Unexpected RunPodSandbox error: %v", err) + } + return + } + // Make sure the sandbox is cleaned up. + defer func() { + assert.NoError(t, runtimeService.StopPodSandbox(sb)) + assert.NoError(t, runtimeService.RemovePodSandbox(sb)) + }() + if test.expectErr { + t.Fatalf("Expected RunPodSandbox to return error") + } + + const ( + testImage = "busybox" + containerName = "test-container" + ) + t.Logf("Pull test image %q", testImage) + img, err := imageService.PullImage(&runtime.ImageSpec{Image: testImage}, nil, sbConfig) + require.NoError(t, err) + defer func() { + assert.NoError(t, imageService.RemoveImage(&runtime.ImageSpec{Image: img})) + }() + + t.Log("Create a container to print env") + cnConfig := ContainerConfig( + containerName, + testImage, + WithCommand("sh", "-c", + "echo -n /etc/hostname= && cat /etc/hostname && env"), + WithLogPath(containerName), + ) + cn, err := runtimeService.CreateContainer(sb, cnConfig, sbConfig) + require.NoError(t, err) + + t.Log("Start the container") + require.NoError(t, runtimeService.StartContainer(cn)) + + t.Log("Wait for container to finish running") + require.NoError(t, Eventually(func() (bool, error) { + s, err := runtimeService.ContainerStatus(cn) + if err != nil { + return false, err + } + if s.GetState() == runtime.ContainerState_CONTAINER_EXITED { + return true, nil + } + return false, nil + }, time.Second, 30*time.Second)) + + content, err := ioutil.ReadFile(filepath.Join(testPodLogDir, containerName)) + assert.NoError(t, err) + + t.Log("Search hostname env in container log") + assert.Contains(t, string(content), "HOSTNAME="+test.expectedHostname) + + t.Log("Search /etc/hostname content in container log") + assert.Contains(t, string(content), "/etc/hostname="+test.expectedHostname) + }) + } +} diff --git a/integration/remote/doc.go b/integration/remote/doc.go new file mode 100644 index 000000000..5b8260831 --- /dev/null +++ b/integration/remote/doc.go @@ -0,0 +1,35 @@ +/* + Copyright The containerd 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 + + http://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. +*/ + +/* +Copyright 2016 The Kubernetes 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 + + http://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. +*/ + +// Package remote contains gRPC implementation of internalapi.RuntimeService +// and internalapi.ImageManagerService. +package remote diff --git a/integration/remote/remote_image.go b/integration/remote/remote_image.go new file mode 100644 index 000000000..121227786 --- /dev/null +++ b/integration/remote/remote_image.go @@ -0,0 +1,172 @@ +/* + Copyright The containerd 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 + + http://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. +*/ + +/* +Copyright 2016 The Kubernetes 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 + + http://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. +*/ + +package remote + +import ( + "context" + "errors" + "fmt" + "time" + + "google.golang.org/grpc" + "k8s.io/klog/v2" + + internalapi "k8s.io/cri-api/pkg/apis" + runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" + + "github.com/containerd/containerd/integration/remote/util" +) + +// ImageService is a gRPC implementation of internalapi.ImageManagerService. +type ImageService struct { + timeout time.Duration + imageClient runtimeapi.ImageServiceClient +} + +// NewImageService creates a new internalapi.ImageManagerService. +func NewImageService(endpoint string, connectionTimeout time.Duration) (internalapi.ImageManagerService, error) { + klog.V(3).Infof("Connecting to image service %s", endpoint) + addr, dialer, err := util.GetAddressAndDialer(endpoint) + if err != nil { + return nil, err + } + + ctx, cancel := context.WithTimeout(context.Background(), connectionTimeout) + defer cancel() + + conn, err := grpc.DialContext(ctx, addr, grpc.WithInsecure(), grpc.WithContextDialer(dialer), grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(maxMsgSize))) + if err != nil { + klog.Errorf("Connect remote image service %s failed: %v", addr, err) + return nil, err + } + + return &ImageService{ + timeout: connectionTimeout, + imageClient: runtimeapi.NewImageServiceClient(conn), + }, nil +} + +// ListImages lists available images. +func (r *ImageService) ListImages(filter *runtimeapi.ImageFilter) ([]*runtimeapi.Image, error) { + ctx, cancel := getContextWithTimeout(r.timeout) + defer cancel() + + resp, err := r.imageClient.ListImages(ctx, &runtimeapi.ListImagesRequest{ + Filter: filter, + }) + if err != nil { + klog.Errorf("ListImages with filter %+v from image service failed: %v", filter, err) + return nil, err + } + + return resp.Images, nil +} + +// ImageStatus returns the status of the image. +func (r *ImageService) ImageStatus(image *runtimeapi.ImageSpec) (*runtimeapi.Image, error) { + ctx, cancel := getContextWithTimeout(r.timeout) + defer cancel() + + resp, err := r.imageClient.ImageStatus(ctx, &runtimeapi.ImageStatusRequest{ + Image: image, + }) + if err != nil { + klog.Errorf("ImageStatus %q from image service failed: %v", image.Image, err) + return nil, err + } + + if resp.Image != nil { + if resp.Image.Id == "" || resp.Image.Size_ == 0 { + errorMessage := fmt.Sprintf("Id or size of image %q is not set", image.Image) + klog.Errorf("ImageStatus failed: %s", errorMessage) + return nil, errors.New(errorMessage) + } + } + + return resp.Image, nil +} + +// PullImage pulls an image with authentication config. +func (r *ImageService) PullImage(image *runtimeapi.ImageSpec, auth *runtimeapi.AuthConfig, podSandboxConfig *runtimeapi.PodSandboxConfig) (string, error) { + ctx, cancel := getContextWithCancel() + defer cancel() + + resp, err := r.imageClient.PullImage(ctx, &runtimeapi.PullImageRequest{ + Image: image, + Auth: auth, + SandboxConfig: podSandboxConfig, + }) + if err != nil { + klog.Errorf("PullImage %q from image service failed: %v", image.Image, err) + return "", err + } + + if resp.ImageRef == "" { + errorMessage := fmt.Sprintf("imageRef of image %q is not set", image.Image) + klog.Errorf("PullImage failed: %s", errorMessage) + return "", errors.New(errorMessage) + } + + return resp.ImageRef, nil +} + +// RemoveImage removes the image. +func (r *ImageService) RemoveImage(image *runtimeapi.ImageSpec) error { + ctx, cancel := getContextWithTimeout(r.timeout) + defer cancel() + + _, err := r.imageClient.RemoveImage(ctx, &runtimeapi.RemoveImageRequest{ + Image: image, + }) + if err != nil { + klog.Errorf("RemoveImage %q from image service failed: %v", image.Image, err) + return err + } + + return nil +} + +// ImageFsInfo returns information of the filesystem that is used to store images. +func (r *ImageService) ImageFsInfo() ([]*runtimeapi.FilesystemUsage, error) { + // Do not set timeout, because `ImageFsInfo` takes time. + // TODO(random-liu): Should we assume runtime should cache the result, and set timeout here? + ctx, cancel := getContextWithCancel() + defer cancel() + + resp, err := r.imageClient.ImageFsInfo(ctx, &runtimeapi.ImageFsInfoRequest{}) + if err != nil { + klog.Errorf("ImageFsInfo from image service failed: %v", err) + return nil, err + } + return resp.GetImageFilesystems(), nil +} diff --git a/integration/remote/remote_runtime.go b/integration/remote/remote_runtime.go new file mode 100644 index 000000000..dce9b9634 --- /dev/null +++ b/integration/remote/remote_runtime.go @@ -0,0 +1,586 @@ +/* + Copyright The containerd 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 + + http://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. +*/ + +/* +Copyright 2016 The Kubernetes 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 + + http://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. +*/ + +package remote + +import ( + "context" + "errors" + "fmt" + "strings" + "time" + + "google.golang.org/grpc" + "k8s.io/klog/v2" + + "k8s.io/component-base/logs/logreduction" + internalapi "k8s.io/cri-api/pkg/apis" + runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" + utilexec "k8s.io/utils/exec" + + "github.com/containerd/containerd/integration/remote/util" +) + +// RuntimeService is a gRPC implementation of internalapi.RuntimeService. +type RuntimeService struct { + timeout time.Duration + runtimeClient runtimeapi.RuntimeServiceClient + // Cache last per-container error message to reduce log spam + logReduction *logreduction.LogReduction +} + +const ( + // How frequently to report identical errors + identicalErrorDelay = 1 * time.Minute +) + +// NewRuntimeService creates a new internalapi.RuntimeService. +func NewRuntimeService(endpoint string, connectionTimeout time.Duration) (internalapi.RuntimeService, error) { + klog.V(3).Infof("Connecting to runtime service %s", endpoint) + addr, dialer, err := util.GetAddressAndDialer(endpoint) + if err != nil { + return nil, err + } + ctx, cancel := context.WithTimeout(context.Background(), connectionTimeout) + defer cancel() + + conn, err := grpc.DialContext(ctx, addr, grpc.WithInsecure(), grpc.WithContextDialer(dialer), grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(maxMsgSize))) + if err != nil { + klog.Errorf("Connect remote runtime %s failed: %v", addr, err) + return nil, err + } + + return &RuntimeService{ + timeout: connectionTimeout, + runtimeClient: runtimeapi.NewRuntimeServiceClient(conn), + logReduction: logreduction.NewLogReduction(identicalErrorDelay), + }, nil +} + +// Version returns the runtime name, runtime version and runtime API version. +func (r *RuntimeService) Version(apiVersion string) (*runtimeapi.VersionResponse, error) { + klog.V(10).Infof("[RuntimeService] Version (apiVersion=%v, timeout=%v)", apiVersion, r.timeout) + + ctx, cancel := getContextWithTimeout(r.timeout) + defer cancel() + + typedVersion, err := r.runtimeClient.Version(ctx, &runtimeapi.VersionRequest{ + Version: apiVersion, + }) + if err != nil { + klog.Errorf("Version from runtime service failed: %v", err) + return nil, err + } + + klog.V(10).Infof("[RuntimeService] Version Response (typedVersion=%v)", typedVersion) + + if typedVersion.Version == "" || typedVersion.RuntimeName == "" || typedVersion.RuntimeApiVersion == "" || typedVersion.RuntimeVersion == "" { + return nil, fmt.Errorf("not all fields are set in VersionResponse (%q)", *typedVersion) + } + + return typedVersion, err +} + +// RunPodSandbox creates and starts a pod-level sandbox. Runtimes should ensure +// the sandbox is in ready state. +func (r *RuntimeService) RunPodSandbox(config *runtimeapi.PodSandboxConfig, runtimeHandler string) (string, error) { + // Use 2 times longer timeout for sandbox operation (4 mins by default) + // TODO: Make the pod sandbox timeout configurable. + timeout := r.timeout * 2 + + klog.V(10).Infof("[RuntimeService] RunPodSandbox (config=%v, runtimeHandler=%v, timeout=%v)", config, runtimeHandler, timeout) + + ctx, cancel := getContextWithTimeout(timeout) + defer cancel() + + resp, err := r.runtimeClient.RunPodSandbox(ctx, &runtimeapi.RunPodSandboxRequest{ + Config: config, + RuntimeHandler: runtimeHandler, + }) + if err != nil { + klog.Errorf("RunPodSandbox from runtime service failed: %v", err) + return "", err + } + + if resp.PodSandboxId == "" { + errorMessage := fmt.Sprintf("PodSandboxId is not set for sandbox %q", config.GetMetadata()) + klog.Errorf("RunPodSandbox failed: %s", errorMessage) + return "", errors.New(errorMessage) + } + + klog.V(10).Infof("[RuntimeService] RunPodSandbox Response (PodSandboxId=%v)", resp.PodSandboxId) + + return resp.PodSandboxId, nil +} + +// StopPodSandbox stops the sandbox. If there are any running containers in the +// sandbox, they should be forced to termination. +func (r *RuntimeService) StopPodSandbox(podSandBoxID string) error { + klog.V(10).Infof("[RuntimeService] StopPodSandbox (podSandboxID=%v, timeout=%v)", podSandBoxID, r.timeout) + + ctx, cancel := getContextWithTimeout(r.timeout) + defer cancel() + + _, err := r.runtimeClient.StopPodSandbox(ctx, &runtimeapi.StopPodSandboxRequest{ + PodSandboxId: podSandBoxID, + }) + if err != nil { + klog.Errorf("StopPodSandbox %q from runtime service failed: %v", podSandBoxID, err) + return err + } + + klog.V(10).Infof("[RuntimeService] StopPodSandbox Response (podSandboxID=%v)", podSandBoxID) + + return nil +} + +// RemovePodSandbox removes the sandbox. If there are any containers in the +// sandbox, they should be forcibly removed. +func (r *RuntimeService) RemovePodSandbox(podSandBoxID string) error { + klog.V(10).Infof("[RuntimeService] RemovePodSandbox (podSandboxID=%v, timeout=%v)", podSandBoxID, r.timeout) + ctx, cancel := getContextWithTimeout(r.timeout) + defer cancel() + + _, err := r.runtimeClient.RemovePodSandbox(ctx, &runtimeapi.RemovePodSandboxRequest{ + PodSandboxId: podSandBoxID, + }) + if err != nil { + klog.Errorf("RemovePodSandbox %q from runtime service failed: %v", podSandBoxID, err) + return err + } + + klog.V(10).Infof("[RuntimeService] RemovePodSandbox Response (podSandboxID=%v)", podSandBoxID) + + return nil +} + +// PodSandboxStatus returns the status of the PodSandbox. +func (r *RuntimeService) PodSandboxStatus(podSandBoxID string) (*runtimeapi.PodSandboxStatus, error) { + klog.V(10).Infof("[RuntimeService] PodSandboxStatus (podSandboxID=%v, timeout=%v)", podSandBoxID, r.timeout) + ctx, cancel := getContextWithTimeout(r.timeout) + defer cancel() + + resp, err := r.runtimeClient.PodSandboxStatus(ctx, &runtimeapi.PodSandboxStatusRequest{ + PodSandboxId: podSandBoxID, + }) + if err != nil { + return nil, err + } + + klog.V(10).Infof("[RuntimeService] PodSandboxStatus Response (podSandboxID=%v, status=%v)", podSandBoxID, resp.Status) + + if resp.Status != nil { + if err := verifySandboxStatus(resp.Status); err != nil { + return nil, err + } + } + + return resp.Status, nil +} + +// ListPodSandbox returns a list of PodSandboxes. +func (r *RuntimeService) ListPodSandbox(filter *runtimeapi.PodSandboxFilter) ([]*runtimeapi.PodSandbox, error) { + klog.V(10).Infof("[RuntimeService] ListPodSandbox (filter=%v, timeout=%v)", filter, r.timeout) + ctx, cancel := getContextWithTimeout(r.timeout) + defer cancel() + + resp, err := r.runtimeClient.ListPodSandbox(ctx, &runtimeapi.ListPodSandboxRequest{ + Filter: filter, + }) + if err != nil { + klog.Errorf("ListPodSandbox with filter %+v from runtime service failed: %v", filter, err) + return nil, err + } + + klog.V(10).Infof("[RuntimeService] ListPodSandbox Response (filter=%v, items=%v)", filter, resp.Items) + + return resp.Items, nil +} + +// CreateContainer creates a new container in the specified PodSandbox. +func (r *RuntimeService) CreateContainer(podSandBoxID string, config *runtimeapi.ContainerConfig, sandboxConfig *runtimeapi.PodSandboxConfig) (string, error) { + klog.V(10).Infof("[RuntimeService] CreateContainer (podSandBoxID=%v, timeout=%v)", podSandBoxID, r.timeout) + ctx, cancel := getContextWithTimeout(r.timeout) + defer cancel() + + resp, err := r.runtimeClient.CreateContainer(ctx, &runtimeapi.CreateContainerRequest{ + PodSandboxId: podSandBoxID, + Config: config, + SandboxConfig: sandboxConfig, + }) + if err != nil { + klog.Errorf("CreateContainer in sandbox %q from runtime service failed: %v", podSandBoxID, err) + return "", err + } + + klog.V(10).Infof("[RuntimeService] CreateContainer (podSandBoxID=%v, ContainerId=%v)", podSandBoxID, resp.ContainerId) + if resp.ContainerId == "" { + errorMessage := fmt.Sprintf("ContainerId is not set for container %q", config.GetMetadata()) + klog.Errorf("CreateContainer failed: %s", errorMessage) + return "", errors.New(errorMessage) + } + + return resp.ContainerId, nil +} + +// StartContainer starts the container. +func (r *RuntimeService) StartContainer(containerID string) error { + klog.V(10).Infof("[RuntimeService] StartContainer (containerID=%v, timeout=%v)", containerID, r.timeout) + ctx, cancel := getContextWithTimeout(r.timeout) + defer cancel() + + _, err := r.runtimeClient.StartContainer(ctx, &runtimeapi.StartContainerRequest{ + ContainerId: containerID, + }) + if err != nil { + klog.Errorf("StartContainer %q from runtime service failed: %v", containerID, err) + return err + } + klog.V(10).Infof("[RuntimeService] StartContainer Response (containerID=%v)", containerID) + + return nil +} + +// StopContainer stops a running container with a grace period (i.e., timeout). +func (r *RuntimeService) StopContainer(containerID string, timeout int64) error { + klog.V(10).Infof("[RuntimeService] StopContainer (containerID=%v, timeout=%v)", containerID, timeout) + // Use timeout + default timeout (2 minutes) as timeout to leave extra time + // for SIGKILL container and request latency. + t := r.timeout + time.Duration(timeout)*time.Second + ctx, cancel := getContextWithTimeout(t) + defer cancel() + + r.logReduction.ClearID(containerID) + _, err := r.runtimeClient.StopContainer(ctx, &runtimeapi.StopContainerRequest{ + ContainerId: containerID, + Timeout: timeout, + }) + if err != nil { + klog.Errorf("StopContainer %q from runtime service failed: %v", containerID, err) + return err + } + klog.V(10).Infof("[RuntimeService] StopContainer Response (containerID=%v)", containerID) + + return nil +} + +// RemoveContainer removes the container. If the container is running, the container +// should be forced to removal. +func (r *RuntimeService) RemoveContainer(containerID string) error { + klog.V(10).Infof("[RuntimeService] RemoveContainer (containerID=%v, timeout=%v)", containerID, r.timeout) + ctx, cancel := getContextWithTimeout(r.timeout) + defer cancel() + + r.logReduction.ClearID(containerID) + _, err := r.runtimeClient.RemoveContainer(ctx, &runtimeapi.RemoveContainerRequest{ + ContainerId: containerID, + }) + if err != nil { + klog.Errorf("RemoveContainer %q from runtime service failed: %v", containerID, err) + return err + } + klog.V(10).Infof("[RuntimeService] RemoveContainer Response (containerID=%v)", containerID) + + return nil +} + +// ListContainers lists containers by filters. +func (r *RuntimeService) ListContainers(filter *runtimeapi.ContainerFilter) ([]*runtimeapi.Container, error) { + klog.V(10).Infof("[RuntimeService] ListContainers (filter=%v, timeout=%v)", filter, r.timeout) + ctx, cancel := getContextWithTimeout(r.timeout) + defer cancel() + + resp, err := r.runtimeClient.ListContainers(ctx, &runtimeapi.ListContainersRequest{ + Filter: filter, + }) + if err != nil { + klog.Errorf("ListContainers with filter %+v from runtime service failed: %v", filter, err) + return nil, err + } + klog.V(10).Infof("[RuntimeService] ListContainers Response (filter=%v, containers=%v)", filter, resp.Containers) + + return resp.Containers, nil +} + +// ContainerStatus returns the container status. +func (r *RuntimeService) ContainerStatus(containerID string) (*runtimeapi.ContainerStatus, error) { + klog.V(10).Infof("[RuntimeService] ContainerStatus (containerID=%v, timeout=%v)", containerID, r.timeout) + ctx, cancel := getContextWithTimeout(r.timeout) + defer cancel() + + resp, err := r.runtimeClient.ContainerStatus(ctx, &runtimeapi.ContainerStatusRequest{ + ContainerId: containerID, + }) + if err != nil { + // Don't spam the log with endless messages about the same failure. + if r.logReduction.ShouldMessageBePrinted(err.Error(), containerID) { + klog.Errorf("ContainerStatus %q from runtime service failed: %v", containerID, err) + } + return nil, err + } + r.logReduction.ClearID(containerID) + klog.V(10).Infof("[RuntimeService] ContainerStatus Response (containerID=%v, status=%v)", containerID, resp.Status) + + if resp.Status != nil { + if err := verifyContainerStatus(resp.Status); err != nil { + klog.Errorf("ContainerStatus of %q failed: %v", containerID, err) + return nil, err + } + } + + return resp.Status, nil +} + +// UpdateContainerResources updates a containers resource config +func (r *RuntimeService) UpdateContainerResources(containerID string, resources *runtimeapi.LinuxContainerResources) error { + klog.V(10).Infof("[RuntimeService] UpdateContainerResources (containerID=%v, timeout=%v)", containerID, r.timeout) + ctx, cancel := getContextWithTimeout(r.timeout) + defer cancel() + + _, err := r.runtimeClient.UpdateContainerResources(ctx, &runtimeapi.UpdateContainerResourcesRequest{ + ContainerId: containerID, + Linux: resources, + }) + if err != nil { + klog.Errorf("UpdateContainerResources %q from runtime service failed: %v", containerID, err) + return err + } + klog.V(10).Infof("[RuntimeService] UpdateContainerResources Response (containerID=%v)", containerID) + + return nil +} + +// ExecSync executes a command in the container, and returns the stdout output. +// If command exits with a non-zero exit code, an error is returned. +func (r *RuntimeService) ExecSync(containerID string, cmd []string, timeout time.Duration) (stdout []byte, stderr []byte, err error) { + klog.V(10).Infof("[RuntimeService] ExecSync (containerID=%v, timeout=%v)", containerID, timeout) + // Do not set timeout when timeout is 0. + var ctx context.Context + var cancel context.CancelFunc + if timeout != 0 { + // Use timeout + default timeout (2 minutes) as timeout to leave some time for + // the runtime to do cleanup. + ctx, cancel = getContextWithTimeout(r.timeout + timeout) + } else { + ctx, cancel = getContextWithCancel() + } + defer cancel() + + timeoutSeconds := int64(timeout.Seconds()) + req := &runtimeapi.ExecSyncRequest{ + ContainerId: containerID, + Cmd: cmd, + Timeout: timeoutSeconds, + } + resp, err := r.runtimeClient.ExecSync(ctx, req) + if err != nil { + klog.Errorf("ExecSync %s '%s' from runtime service failed: %v", containerID, strings.Join(cmd, " "), err) + return nil, nil, err + } + + klog.V(10).Infof("[RuntimeService] ExecSync Response (containerID=%v, ExitCode=%v)", containerID, resp.ExitCode) + err = nil + if resp.ExitCode != 0 { + err = utilexec.CodeExitError{ + Err: fmt.Errorf("command '%s' exited with %d: %s", strings.Join(cmd, " "), resp.ExitCode, resp.Stderr), + Code: int(resp.ExitCode), + } + } + + return resp.Stdout, resp.Stderr, err +} + +// Exec prepares a streaming endpoint to execute a command in the container, and returns the address. +func (r *RuntimeService) Exec(req *runtimeapi.ExecRequest) (*runtimeapi.ExecResponse, error) { + klog.V(10).Infof("[RuntimeService] Exec (timeout=%v)", r.timeout) + ctx, cancel := getContextWithTimeout(r.timeout) + defer cancel() + + resp, err := r.runtimeClient.Exec(ctx, req) + if err != nil { + klog.Errorf("Exec %s '%s' from runtime service failed: %v", req.ContainerId, strings.Join(req.Cmd, " "), err) + return nil, err + } + klog.V(10).Info("[RuntimeService] Exec Response") + + if resp.Url == "" { + errorMessage := "URL is not set" + klog.Errorf("Exec failed: %s", errorMessage) + return nil, errors.New(errorMessage) + } + + return resp, nil +} + +// Attach prepares a streaming endpoint to attach to a running container, and returns the address. +func (r *RuntimeService) Attach(req *runtimeapi.AttachRequest) (*runtimeapi.AttachResponse, error) { + klog.V(10).Infof("[RuntimeService] Attach (containerId=%v, timeout=%v)", req.ContainerId, r.timeout) + ctx, cancel := getContextWithTimeout(r.timeout) + defer cancel() + + resp, err := r.runtimeClient.Attach(ctx, req) + if err != nil { + klog.Errorf("Attach %s from runtime service failed: %v", req.ContainerId, err) + return nil, err + } + klog.V(10).Infof("[RuntimeService] Attach Response (containerId=%v)", req.ContainerId) + + if resp.Url == "" { + errorMessage := "URL is not set" + klog.Errorf("Attach failed: %s", errorMessage) + return nil, errors.New(errorMessage) + } + return resp, nil +} + +// PortForward prepares a streaming endpoint to forward ports from a PodSandbox, and returns the address. +func (r *RuntimeService) PortForward(req *runtimeapi.PortForwardRequest) (*runtimeapi.PortForwardResponse, error) { + klog.V(10).Infof("[RuntimeService] PortForward (podSandboxID=%v, port=%v, timeout=%v)", req.PodSandboxId, req.Port, r.timeout) + ctx, cancel := getContextWithTimeout(r.timeout) + defer cancel() + + resp, err := r.runtimeClient.PortForward(ctx, req) + if err != nil { + klog.Errorf("PortForward %s from runtime service failed: %v", req.PodSandboxId, err) + return nil, err + } + klog.V(10).Infof("[RuntimeService] PortForward Response (podSandboxID=%v)", req.PodSandboxId) + + if resp.Url == "" { + errorMessage := "URL is not set" + klog.Errorf("PortForward failed: %s", errorMessage) + return nil, errors.New(errorMessage) + } + + return resp, nil +} + +// UpdateRuntimeConfig updates the config of a runtime service. The only +// update payload currently supported is the pod CIDR assigned to a node, +// and the runtime service just proxies it down to the network plugin. +func (r *RuntimeService) UpdateRuntimeConfig(runtimeConfig *runtimeapi.RuntimeConfig) error { + klog.V(10).Infof("[RuntimeService] UpdateRuntimeConfig (runtimeConfig=%v, timeout=%v)", runtimeConfig, r.timeout) + ctx, cancel := getContextWithTimeout(r.timeout) + defer cancel() + + // Response doesn't contain anything of interest. This translates to an + // Event notification to the network plugin, which can't fail, so we're + // really looking to surface destination unreachable. + _, err := r.runtimeClient.UpdateRuntimeConfig(ctx, &runtimeapi.UpdateRuntimeConfigRequest{ + RuntimeConfig: runtimeConfig, + }) + + if err != nil { + return err + } + klog.V(10).Infof("[RuntimeService] UpdateRuntimeConfig Response (runtimeConfig=%v)", runtimeConfig) + + return nil +} + +// Status returns the status of the runtime. +func (r *RuntimeService) Status() (*runtimeapi.RuntimeStatus, error) { + klog.V(10).Infof("[RuntimeService] Status (timeout=%v)", r.timeout) + ctx, cancel := getContextWithTimeout(r.timeout) + defer cancel() + + resp, err := r.runtimeClient.Status(ctx, &runtimeapi.StatusRequest{}) + if err != nil { + klog.Errorf("Status from runtime service failed: %v", err) + return nil, err + } + + klog.V(10).Infof("[RuntimeService] Status Response (status=%v)", resp.Status) + + if resp.Status == nil || len(resp.Status.Conditions) < 2 { + errorMessage := "RuntimeReady or NetworkReady condition are not set" + klog.Errorf("Status failed: %s", errorMessage) + return nil, errors.New(errorMessage) + } + + return resp.Status, nil +} + +// ContainerStats returns the stats of the container. +func (r *RuntimeService) ContainerStats(containerID string) (*runtimeapi.ContainerStats, error) { + klog.V(10).Infof("[RuntimeService] ContainerStats (containerID=%v, timeout=%v)", containerID, r.timeout) + ctx, cancel := getContextWithTimeout(r.timeout) + defer cancel() + + resp, err := r.runtimeClient.ContainerStats(ctx, &runtimeapi.ContainerStatsRequest{ + ContainerId: containerID, + }) + if err != nil { + if r.logReduction.ShouldMessageBePrinted(err.Error(), containerID) { + klog.Errorf("ContainerStats %q from runtime service failed: %v", containerID, err) + } + return nil, err + } + r.logReduction.ClearID(containerID) + klog.V(10).Infof("[RuntimeService] ContainerStats Response (containerID=%v, stats=%v)", containerID, resp.GetStats()) + + return resp.GetStats(), nil +} + +func (r *RuntimeService) ListContainerStats(filter *runtimeapi.ContainerStatsFilter) ([]*runtimeapi.ContainerStats, error) { + klog.V(10).Infof("[RuntimeService] ListContainerStats (filter=%v)", filter) + // Do not set timeout, because writable layer stats collection takes time. + // TODO(random-liu): Should we assume runtime should cache the result, and set timeout here? + ctx, cancel := getContextWithCancel() + defer cancel() + + resp, err := r.runtimeClient.ListContainerStats(ctx, &runtimeapi.ListContainerStatsRequest{ + Filter: filter, + }) + if err != nil { + klog.Errorf("ListContainerStats with filter %+v from runtime service failed: %v", filter, err) + return nil, err + } + klog.V(10).Infof("[RuntimeService] ListContainerStats Response (filter=%v, stats=%v)", filter, resp.GetStats()) + + return resp.GetStats(), nil +} + +func (r *RuntimeService) ReopenContainerLog(containerID string) error { + klog.V(10).Infof("[RuntimeService] ReopenContainerLog (containerID=%v, timeout=%v)", containerID, r.timeout) + ctx, cancel := getContextWithTimeout(r.timeout) + defer cancel() + + _, err := r.runtimeClient.ReopenContainerLog(ctx, &runtimeapi.ReopenContainerLogRequest{ContainerId: containerID}) + if err != nil { + klog.Errorf("ReopenContainerLog %q from runtime service failed: %v", containerID, err) + return err + } + + klog.V(10).Infof("[RuntimeService] ReopenContainerLog Response (containerID=%v)", containerID) + return nil +} diff --git a/integration/remote/util/util_unix.go b/integration/remote/util/util_unix.go new file mode 100644 index 000000000..cc3f85814 --- /dev/null +++ b/integration/remote/util/util_unix.go @@ -0,0 +1,161 @@ +// +build freebsd linux darwin + +/* + Copyright The containerd 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 + + http://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. +*/ + +/* +Copyright 2017 The Kubernetes 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 + + http://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. +*/ + +package util + +import ( + "context" + "fmt" + "io/ioutil" + "net" + "net/url" + "os" + "path/filepath" + + "golang.org/x/sys/unix" + "k8s.io/klog/v2" +) + +const ( + // unixProtocol is the network protocol of unix socket. + unixProtocol = "unix" +) + +// CreateListener creates a listener on the specified endpoint. +func CreateListener(endpoint string) (net.Listener, error) { + protocol, addr, err := parseEndpointWithFallbackProtocol(endpoint, unixProtocol) + if err != nil { + return nil, err + } + if protocol != unixProtocol { + return nil, fmt.Errorf("only support unix socket endpoint") + } + + // Unlink to cleanup the previous socket file. + err = unix.Unlink(addr) + if err != nil && !os.IsNotExist(err) { + return nil, fmt.Errorf("failed to unlink socket file %q: %v", addr, err) + } + + if err := os.MkdirAll(filepath.Dir(addr), 0750); err != nil { + return nil, fmt.Errorf("error creating socket directory %q: %v", filepath.Dir(addr), err) + } + + // Create the socket on a tempfile and move it to the destination socket to handle improprer cleanup + file, err := ioutil.TempFile(filepath.Dir(addr), "") + if err != nil { + return nil, fmt.Errorf("failed to create temporary file: %v", err) + } + + if err := os.Remove(file.Name()); err != nil { + return nil, fmt.Errorf("failed to remove temporary file: %v", err) + } + + l, err := net.Listen(protocol, file.Name()) + if err != nil { + return nil, err + } + + if err = os.Rename(file.Name(), addr); err != nil { + return nil, fmt.Errorf("failed to move temporary file to addr %q: %v", addr, err) + } + + return l, nil +} + +// GetAddressAndDialer returns the address parsed from the given endpoint and a context dialer. +func GetAddressAndDialer(endpoint string) (string, func(ctx context.Context, addr string) (net.Conn, error), error) { + protocol, addr, err := parseEndpointWithFallbackProtocol(endpoint, unixProtocol) + if err != nil { + return "", nil, err + } + if protocol != unixProtocol { + return "", nil, fmt.Errorf("only support unix socket endpoint") + } + + return addr, dial, nil +} + +func dial(ctx context.Context, addr string) (net.Conn, error) { + return (&net.Dialer{}).DialContext(ctx, unixProtocol, addr) +} + +func parseEndpointWithFallbackProtocol(endpoint string, fallbackProtocol string) (protocol string, addr string, err error) { + if protocol, addr, err = parseEndpoint(endpoint); err != nil && protocol == "" { + fallbackEndpoint := fallbackProtocol + "://" + endpoint + protocol, addr, err = parseEndpoint(fallbackEndpoint) + if err == nil { + klog.Warningf("Using %q as endpoint is deprecated, please consider using full url format %q.", endpoint, fallbackEndpoint) + } + } + return +} + +func parseEndpoint(endpoint string) (string, string, error) { + u, err := url.Parse(endpoint) + if err != nil { + return "", "", err + } + + switch u.Scheme { + case "tcp": + return "tcp", u.Host, nil + + case "unix": + return "unix", u.Path, nil + + case "": + return "", "", fmt.Errorf("using %q as endpoint is deprecated, please consider using full url format", endpoint) + + default: + return u.Scheme, "", fmt.Errorf("protocol %q not supported", u.Scheme) + } +} + +// IsUnixDomainSocket returns whether a given file is a AF_UNIX socket file +func IsUnixDomainSocket(filePath string) (bool, error) { + fi, err := os.Stat(filePath) + if err != nil { + return false, fmt.Errorf("stat file %s failed: %v", filePath, err) + } + if fi.Mode()&os.ModeSocket == 0 { + return false, nil + } + return true, nil +} + +// NormalizePath is a no-op for Linux for now +func NormalizePath(path string) string { + return path +} diff --git a/integration/remote/util/util_unsupported.go b/integration/remote/util/util_unsupported.go new file mode 100644 index 000000000..81f412172 --- /dev/null +++ b/integration/remote/util/util_unsupported.go @@ -0,0 +1,71 @@ +// +build !freebsd,!linux,!windows,!darwin + +/* + Copyright The containerd 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 + + http://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. +*/ + +/* +Copyright 2017 The Kubernetes 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 + + http://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. +*/ + +package util + +import ( + "context" + "fmt" + "net" + "time" +) + +// CreateListener creates a listener on the specified endpoint. +func CreateListener(endpoint string) (net.Listener, error) { + return nil, fmt.Errorf("CreateListener is unsupported in this build") +} + +// GetAddressAndDialer returns the address parsed from the given endpoint and a context dialer. +func GetAddressAndDialer(endpoint string) (string, func(ctx context.Context, addr string) (net.Conn, error), error) { + return "", nil, fmt.Errorf("GetAddressAndDialer is unsupported in this build") +} + +// LockAndCheckSubPath empty implementation +func LockAndCheckSubPath(volumePath, subPath string) ([]uintptr, error) { + return []uintptr{}, nil +} + +// UnlockPath empty implementation +func UnlockPath(fileHandles []uintptr) { +} + +// LocalEndpoint empty implementation +func LocalEndpoint(path, file string) (string, error) { + return "", fmt.Errorf("LocalEndpoints are unsupported in this build") +} + +// GetBootTime empty implementation +func GetBootTime() (time.Time, error) { + return time.Time{}, fmt.Errorf("GetBootTime is unsupported in this build") +} diff --git a/integration/remote/util/util_windows.go b/integration/remote/util/util_windows.go new file mode 100644 index 000000000..979ebf2fc --- /dev/null +++ b/integration/remote/util/util_windows.go @@ -0,0 +1,165 @@ +// +build windows + +/* + Copyright The containerd 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 + + http://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. +*/ + +/* +Copyright 2017 The Kubernetes 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 + + http://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. +*/ + +package util + +import ( + "context" + "fmt" + "net" + "net/url" + "strings" + "syscall" + "time" + + "github.com/Microsoft/go-winio" +) + +const ( + tcpProtocol = "tcp" + npipeProtocol = "npipe" +) + +// CreateListener creates a listener on the specified endpoint. +func CreateListener(endpoint string) (net.Listener, error) { + protocol, addr, err := parseEndpoint(endpoint) + if err != nil { + return nil, err + } + + switch protocol { + case tcpProtocol: + return net.Listen(tcpProtocol, addr) + + case npipeProtocol: + return winio.ListenPipe(addr, nil) + + default: + return nil, fmt.Errorf("only support tcp and npipe endpoint") + } +} + +// GetAddressAndDialer returns the address parsed from the given endpoint and a context dialer. +func GetAddressAndDialer(endpoint string) (string, func(ctx context.Context, addr string) (net.Conn, error), error) { + protocol, addr, err := parseEndpoint(endpoint) + if err != nil { + return "", nil, err + } + + if protocol == tcpProtocol { + return addr, tcpDial, nil + } + + if protocol == npipeProtocol { + return addr, npipeDial, nil + } + + return "", nil, fmt.Errorf("only support tcp and npipe endpoint") +} + +func tcpDial(ctx context.Context, addr string) (net.Conn, error) { + return (&net.Dialer{}).DialContext(ctx, tcpProtocol, addr) +} + +func npipeDial(ctx context.Context, addr string) (net.Conn, error) { + return winio.DialPipeContext(ctx, addr) +} + +func parseEndpoint(endpoint string) (string, string, error) { + // url.Parse doesn't recognize \, so replace with / first. + endpoint = strings.Replace(endpoint, "\\", "/", -1) + u, err := url.Parse(endpoint) + if err != nil { + return "", "", err + } + + if u.Scheme == "tcp" { + return "tcp", u.Host, nil + } else if u.Scheme == "npipe" { + if strings.HasPrefix(u.Path, "//./pipe") { + return "npipe", u.Path, nil + } + + // fallback host if not provided. + host := u.Host + if host == "" { + host = "." + } + return "npipe", fmt.Sprintf("//%s%s", host, u.Path), nil + } else if u.Scheme == "" { + return "", "", fmt.Errorf("Using %q as endpoint is deprecated, please consider using full url format", endpoint) + } else { + return u.Scheme, "", fmt.Errorf("protocol %q not supported", u.Scheme) + } +} + +var tickCount = syscall.NewLazyDLL("kernel32.dll").NewProc("GetTickCount64") + +// GetBootTime returns the time at which the machine was started, truncated to the nearest second +func GetBootTime() (time.Time, error) { + currentTime := time.Now() + output, _, err := tickCount.Call() + if errno, ok := err.(syscall.Errno); !ok || errno != 0 { + return time.Time{}, err + } + return currentTime.Add(-time.Duration(output) * time.Millisecond).Truncate(time.Second), nil +} + +// IsUnixDomainSocket returns whether a given file is a AF_UNIX socket file +func IsUnixDomainSocket(filePath string) (bool, error) { + // Due to the absence of golang support for os.ModeSocket in Windows (https://github.com/golang/go/issues/33357) + // we need to dial the file and check if we receive an error to determine if a file is Unix Domain Socket file. + + // Note that querrying for the Reparse Points (https://docs.microsoft.com/en-us/windows/win32/fileio/reparse-points) + // for the file (using FSCTL_GET_REPARSE_POINT) and checking for reparse tag: reparseTagSocket + // does NOT work in 1809 if the socket file is created within a bind mounted directory by a container + // and the FSCTL is issued in the host by the kubelet. + + c, err := net.Dial("unix", filePath) + if err == nil { + c.Close() + return true, nil + } + return false, nil +} + +// NormalizePath converts FS paths returned by certain go frameworks (like fsnotify) +// to native Windows paths that can be passed to Windows specific code +func NormalizePath(path string) string { + path = strings.ReplaceAll(path, "/", "\\") + if strings.HasPrefix(path, "\\") { + path = "c:" + path + } + return path +} diff --git a/integration/remote/utils.go b/integration/remote/utils.go new file mode 100644 index 000000000..a1390c57b --- /dev/null +++ b/integration/remote/utils.go @@ -0,0 +1,107 @@ +/* + Copyright The containerd 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 + + http://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. +*/ + +/* +Copyright 2016 The Kubernetes 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 + + http://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. +*/ + +package remote + +import ( + "context" + "fmt" + "time" + + runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" +) + +// maxMsgSize use 16MB as the default message size limit. +// grpc library default is 4MB +const maxMsgSize = 1024 * 1024 * 16 + +// getContextWithTimeout returns a context with timeout. +func getContextWithTimeout(timeout time.Duration) (context.Context, context.CancelFunc) { + return context.WithTimeout(context.Background(), timeout) +} + +// getContextWithCancel returns a context with cancel. +func getContextWithCancel() (context.Context, context.CancelFunc) { + return context.WithCancel(context.Background()) +} + +// verifySandboxStatus verified whether all required fields are set in PodSandboxStatus. +func verifySandboxStatus(status *runtimeapi.PodSandboxStatus) error { + if status.Id == "" { + return fmt.Errorf("Id is not set") + } + + if status.Metadata == nil { + return fmt.Errorf("Metadata is not set") + } + + metadata := status.Metadata + if metadata.Name == "" || metadata.Namespace == "" || metadata.Uid == "" { + return fmt.Errorf("Name, Namespace or Uid is not in metadata %q", metadata) + } + + if status.CreatedAt == 0 { + return fmt.Errorf("CreatedAt is not set") + } + + return nil +} + +// verifyContainerStatus verified whether all required fields are set in ContainerStatus. +func verifyContainerStatus(status *runtimeapi.ContainerStatus) error { + if status.Id == "" { + return fmt.Errorf("Id is not set") + } + + if status.Metadata == nil { + return fmt.Errorf("Metadata is not set") + } + + metadata := status.Metadata + if metadata.Name == "" { + return fmt.Errorf("Name is not in metadata %q", metadata) + } + + if status.CreatedAt == 0 { + return fmt.Errorf("CreatedAt is not set") + } + + if status.Image == nil || status.Image.Image == "" { + return fmt.Errorf("Image is not set") + } + + if status.ImageRef == "" { + return fmt.Errorf("ImageRef is not set") + } + + return nil +} diff --git a/integration/restart_test.go b/integration/restart_test.go new file mode 100644 index 000000000..9c9051193 --- /dev/null +++ b/integration/restart_test.go @@ -0,0 +1,201 @@ +// +build linux + +/* + Copyright The containerd 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 + + http://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. +*/ + +package integration + +import ( + "sort" + "testing" + + "github.com/containerd/containerd" + "github.com/containerd/containerd/errdefs" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/net/context" + runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" +) + +// Restart test must run sequentially. + +func TestContainerdRestart(t *testing.T) { + type container struct { + name string + id string + state runtime.ContainerState + } + type sandbox struct { + name string + id string + state runtime.PodSandboxState + containers []container + } + ctx := context.Background() + sandboxNS := "restart-containerd" + sandboxes := []sandbox{ + { + name: "ready-sandbox", + state: runtime.PodSandboxState_SANDBOX_READY, + containers: []container{ + { + name: "created-container", + state: runtime.ContainerState_CONTAINER_CREATED, + }, + { + name: "running-container", + state: runtime.ContainerState_CONTAINER_RUNNING, + }, + { + name: "exited-container", + state: runtime.ContainerState_CONTAINER_EXITED, + }, + }, + }, + { + name: "notready-sandbox", + state: runtime.PodSandboxState_SANDBOX_NOTREADY, + containers: []container{ + { + name: "created-container", + state: runtime.ContainerState_CONTAINER_CREATED, + }, + { + name: "running-container", + state: runtime.ContainerState_CONTAINER_RUNNING, + }, + { + name: "exited-container", + state: runtime.ContainerState_CONTAINER_EXITED, + }, + }, + }, + } + t.Logf("Make sure no sandbox is running before test") + existingSandboxes, err := runtimeService.ListPodSandbox(&runtime.PodSandboxFilter{}) + require.NoError(t, err) + require.Empty(t, existingSandboxes) + + t.Logf("Start test sandboxes and containers") + for i := range sandboxes { + s := &sandboxes[i] + sbCfg := PodSandboxConfig(s.name, sandboxNS) + sid, err := runtimeService.RunPodSandbox(sbCfg, *runtimeHandler) + require.NoError(t, err) + defer func() { + // Make sure the sandbox is cleaned up in any case. + runtimeService.StopPodSandbox(sid) + runtimeService.RemovePodSandbox(sid) + }() + s.id = sid + for j := range s.containers { + c := &s.containers[j] + cfg := ContainerConfig(c.name, pauseImage, + // Set pid namespace as per container, so that container won't die + // when sandbox container is killed. + WithPidNamespace(runtime.NamespaceMode_CONTAINER), + ) + cid, err := runtimeService.CreateContainer(sid, cfg, sbCfg) + require.NoError(t, err) + // Reply on sandbox cleanup. + c.id = cid + switch c.state { + case runtime.ContainerState_CONTAINER_CREATED: + case runtime.ContainerState_CONTAINER_RUNNING: + require.NoError(t, runtimeService.StartContainer(cid)) + case runtime.ContainerState_CONTAINER_EXITED: + require.NoError(t, runtimeService.StartContainer(cid)) + require.NoError(t, runtimeService.StopContainer(cid, 10)) + } + } + if s.state == runtime.PodSandboxState_SANDBOX_NOTREADY { + cntr, err := containerdClient.LoadContainer(ctx, sid) + require.NoError(t, err) + task, err := cntr.Task(ctx, nil) + require.NoError(t, err) + _, err = task.Delete(ctx, containerd.WithProcessKill) + if err != nil { + require.True(t, errdefs.IsNotFound(err)) + } + } + } + + t.Logf("Pull test images") + for _, image := range []string{"busybox", "alpine"} { + img, err := imageService.PullImage(&runtime.ImageSpec{Image: image}, nil, nil) + require.NoError(t, err) + defer func() { + assert.NoError(t, imageService.RemoveImage(&runtime.ImageSpec{Image: img})) + }() + } + imagesBeforeRestart, err := imageService.ListImages(nil) + assert.NoError(t, err) + + t.Logf("Restart containerd") + RestartContainerd(t) + + t.Logf("Check sandbox and container state after restart") + loadedSandboxes, err := runtimeService.ListPodSandbox(&runtime.PodSandboxFilter{}) + require.NoError(t, err) + assert.Len(t, loadedSandboxes, len(sandboxes)) + loadedContainers, err := runtimeService.ListContainers(&runtime.ContainerFilter{}) + require.NoError(t, err) + assert.Len(t, loadedContainers, len(sandboxes)*3) + for _, s := range sandboxes { + for _, loaded := range loadedSandboxes { + if s.id == loaded.Id { + assert.Equal(t, s.state, loaded.State) + break + } + } + for _, c := range s.containers { + for _, loaded := range loadedContainers { + if c.id == loaded.Id { + assert.Equal(t, c.state, loaded.State) + break + } + } + } + } + + t.Logf("Should be able to stop and remove sandbox after restart") + for _, s := range sandboxes { + assert.NoError(t, runtimeService.StopPodSandbox(s.id)) + assert.NoError(t, runtimeService.RemovePodSandbox(s.id)) + } + + t.Logf("Should recover all images") + imagesAfterRestart, err := imageService.ListImages(nil) + assert.NoError(t, err) + assert.Equal(t, len(imagesBeforeRestart), len(imagesAfterRestart)) + for _, i1 := range imagesBeforeRestart { + found := false + for _, i2 := range imagesAfterRestart { + if i1.Id == i2.Id { + sort.Strings(i1.RepoTags) + sort.Strings(i1.RepoDigests) + sort.Strings(i2.RepoTags) + sort.Strings(i2.RepoDigests) + assert.Equal(t, i1, i2) + found = true + break + } + } + assert.True(t, found, "should find image %+v", i1) + } +} + +// TODO: Add back the unknown state test. diff --git a/integration/runtime_handler_test.go b/integration/runtime_handler_test.go new file mode 100644 index 000000000..f07bf7b90 --- /dev/null +++ b/integration/runtime_handler_test.go @@ -0,0 +1,52 @@ +// +build linux + +/* + Copyright The containerd 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 + + http://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. +*/ + +package integration + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" +) + +func TestRuntimeHandler(t *testing.T) { + t.Logf("Create a sandbox") + sbConfig := PodSandboxConfig("sandbox", "test-runtime-handler") + t.Logf("the --runtime-handler flag value is: %s", *runtimeHandler) + sb, err := runtimeService.RunPodSandbox(sbConfig, *runtimeHandler) + require.NoError(t, err) + defer func() { + // Make sure the sandbox is cleaned up in any case. + runtimeService.StopPodSandbox(sb) + runtimeService.RemovePodSandbox(sb) + }() + + t.Logf("Verify runtimeService.PodSandboxStatus sets RuntimeHandler") + sbStatus, err := runtimeService.PodSandboxStatus(sb) + require.NoError(t, err) + t.Logf("runtimeService.PodSandboxStatus sets RuntimeHandler to %s", sbStatus.RuntimeHandler) + assert.Equal(t, *runtimeHandler, sbStatus.RuntimeHandler) + + t.Logf("Verify runtimeService.ListPodSandbox sets RuntimeHandler") + sandboxes, err := runtimeService.ListPodSandbox(&runtime.PodSandboxFilter{}) + require.NoError(t, err) + t.Logf("runtimeService.ListPodSandbox sets RuntimeHandler to %s", sbStatus.RuntimeHandler) + assert.Equal(t, *runtimeHandler, sandboxes[0].RuntimeHandler) +} diff --git a/integration/sandbox_clean_remove_test.go b/integration/sandbox_clean_remove_test.go new file mode 100644 index 000000000..f74c145cc --- /dev/null +++ b/integration/sandbox_clean_remove_test.go @@ -0,0 +1,126 @@ +// +build linux + +/* + Copyright The containerd 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 + + http://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. +*/ + +package integration + +import ( + "io/ioutil" + "os" + "path/filepath" + "strings" + "testing" + "time" + + runtimespec "github.com/opencontainers/runtime-spec/specs-go" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/sys/unix" + runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" +) + +func TestSandboxRemoveWithoutIPLeakage(t *testing.T) { + const hostLocalCheckpointDir = "/var/lib/cni" + + t.Logf("Make sure host-local ipam is in use") + config, err := CRIConfig() + require.NoError(t, err) + fs, err := ioutil.ReadDir(config.NetworkPluginConfDir) + require.NoError(t, err) + require.NotEmpty(t, fs) + f := filepath.Join(config.NetworkPluginConfDir, fs[0].Name()) + cniConfig, err := ioutil.ReadFile(f) + require.NoError(t, err) + if !strings.Contains(string(cniConfig), "host-local") { + t.Skip("host-local ipam is not in use") + } + + t.Logf("Create a sandbox") + sbConfig := PodSandboxConfig("sandbox", "remove-without-ip-leakage") + sb, err := runtimeService.RunPodSandbox(sbConfig, *runtimeHandler) + require.NoError(t, err) + defer func() { + // Make sure the sandbox is cleaned up in any case. + runtimeService.StopPodSandbox(sb) + runtimeService.RemovePodSandbox(sb) + }() + + t.Logf("Get pod information") + status, info, err := SandboxInfo(sb) + require.NoError(t, err) + ip := status.GetNetwork().GetIp() + require.NotEmpty(t, ip) + require.NotNil(t, info.RuntimeSpec.Linux) + var netNS string + for _, n := range info.RuntimeSpec.Linux.Namespaces { + if n.Type == runtimespec.NetworkNamespace { + netNS = n.Path + } + } + require.NotEmpty(t, netNS, "network namespace should be set") + + t.Logf("Should be able to find the pod ip in host-local checkpoint") + checkIP := func(ip string) bool { + found := false + filepath.Walk(hostLocalCheckpointDir, func(_ string, info os.FileInfo, _ error) error { + if info != nil && info.Name() == ip { + found = true + } + return nil + }) + return found + } + require.True(t, checkIP(ip)) + + t.Logf("Kill sandbox container") + require.NoError(t, KillPid(int(info.Pid))) + + t.Logf("Unmount network namespace") + require.NoError(t, unix.Unmount(netNS, unix.MNT_DETACH)) + + t.Logf("Network namespace should be closed") + _, info, err = SandboxInfo(sb) + require.NoError(t, err) + assert.True(t, info.NetNSClosed) + + t.Logf("Remove network namespace") + require.NoError(t, os.RemoveAll(netNS)) + + t.Logf("Network namespace should still be closed") + _, info, err = SandboxInfo(sb) + require.NoError(t, err) + assert.True(t, info.NetNSClosed) + + t.Logf("Sandbox state should be NOTREADY") + assert.NoError(t, Eventually(func() (bool, error) { + status, err := runtimeService.PodSandboxStatus(sb) + if err != nil { + return false, err + } + return status.GetState() == runtime.PodSandboxState_SANDBOX_NOTREADY, nil + }, time.Second, 30*time.Second), "sandbox state should become NOTREADY") + + t.Logf("Should still be able to find the pod ip in host-local checkpoint") + assert.True(t, checkIP(ip)) + + t.Logf("Should be able to stop and remove the sandbox") + assert.NoError(t, runtimeService.StopPodSandbox(sb)) + assert.NoError(t, runtimeService.RemovePodSandbox(sb)) + + t.Logf("Should not be able to find the pod ip in host-local checkpoint") + assert.False(t, checkIP(ip)) +} diff --git a/integration/truncindex_test.go b/integration/truncindex_test.go new file mode 100644 index 000000000..f3e52300b --- /dev/null +++ b/integration/truncindex_test.go @@ -0,0 +1,160 @@ +// +build linux + +/* + Copyright The containerd 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 + + http://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. +*/ + +package integration + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" +) + +func genTruncIndex(normalName string) string { + return normalName[:(len(normalName)+1)/2] +} + +func TestTruncIndex(t *testing.T) { + sbConfig := PodSandboxConfig("sandbox", "truncindex") + + t.Logf("Pull an image") + const appImage = "busybox" + imgID, err := imageService.PullImage(&runtimeapi.ImageSpec{Image: appImage}, nil, sbConfig) + require.NoError(t, err) + imgTruncID := genTruncIndex(imgID) + defer func() { + assert.NoError(t, imageService.RemoveImage(&runtimeapi.ImageSpec{Image: imgTruncID})) + }() + + t.Logf("Get image status by truncindex, truncID: %s", imgTruncID) + res, err := imageService.ImageStatus(&runtimeapi.ImageSpec{Image: imgTruncID}) + require.NoError(t, err) + require.NotEqual(t, nil, res) + assert.Equal(t, imgID, res.Id) + + // TODO(yanxuean): for failure test case where there are two images with the same truncindex. + // if you add n images at least two will share the same leading digit. + // "sha256:n" where n is the a number from 0-9 where two images have the same trunc, + // for example sha256:9 + // https://github.com/containerd/cri/pull/352 + // I am thinking how I get the two image which have same trunc. + + // TODO(yanxuean): add test case for ListImages + + t.Logf("Create a sandbox") + sb, err := runtimeService.RunPodSandbox(sbConfig, *runtimeHandler) + require.NoError(t, err) + sbTruncIndex := genTruncIndex(sb) + var hasStoppedSandbox bool + defer func() { + // The 2th StopPodSandbox will fail, the 2th RemovePodSandbox will success. + if !hasStoppedSandbox { + assert.NoError(t, runtimeService.StopPodSandbox(sbTruncIndex)) + } + assert.NoError(t, runtimeService.RemovePodSandbox(sbTruncIndex)) + }() + + t.Logf("Get sandbox status by truncindex") + sbStatus, err := runtimeService.PodSandboxStatus(sbTruncIndex) + require.NoError(t, err) + assert.Equal(t, sb, sbStatus.Id) + + t.Logf("Forward port for sandbox by truncindex") + _, err = runtimeService.PortForward(&runtimeapi.PortForwardRequest{PodSandboxId: sbTruncIndex, Port: []int32{80}}) + assert.NoError(t, err) + + // TODO(yanxuean): add test case for ListPodSandbox + + t.Logf("Create a container") + cnConfig := ContainerConfig( + "containerTruncIndex", + appImage, + WithCommand("top"), + ) + cn, err := runtimeService.CreateContainer(sbTruncIndex, cnConfig, sbConfig) + require.NoError(t, err) + cnTruncIndex := genTruncIndex(cn) + defer func() { + // the 2th RemovePodSandbox will success. + assert.NoError(t, runtimeService.RemoveContainer(cnTruncIndex)) + }() + + t.Logf("Get container status by truncindex") + cStatus, err := runtimeService.ContainerStatus(cnTruncIndex) + require.NoError(t, err) + assert.Equal(t, cn, cStatus.Id) + + t.Logf("Start the container") + require.NoError(t, runtimeService.StartContainer(cnTruncIndex)) + var hasStoppedContainer bool + defer func() { + // The 2th StopPodSandbox will fail + if !hasStoppedContainer { + assert.NoError(t, runtimeService.StopContainer(cnTruncIndex, 10)) + } + }() + + t.Logf("Stats the container") + cStats, err := runtimeService.ContainerStats(cnTruncIndex) + require.NoError(t, err) + assert.Equal(t, cn, cStats.Attributes.Id) + + t.Logf("Update container memory limit after started") + err = runtimeService.UpdateContainerResources(cnTruncIndex, &runtimeapi.LinuxContainerResources{ + MemoryLimitInBytes: 50 * 1024 * 1024, + }) + assert.NoError(t, err) + + t.Logf("Execute cmd in container") + execReq := &runtimeapi.ExecRequest{ + ContainerId: cnTruncIndex, + Cmd: []string{"pwd"}, + Stdout: true, + } + _, err = runtimeService.Exec(execReq) + assert.NoError(t, err) + + t.Logf("Execute cmd in container by sync") + _, _, err = runtimeService.ExecSync(cnTruncIndex, []string{"pwd"}, 10) + assert.NoError(t, err) + + // TODO(yanxuean): add test case for ListContainers + + t.Logf("Get a non exist container status by truncindex") + err = runtimeService.StopContainer(cnTruncIndex, 10) + assert.NoError(t, err) + if err == nil { + hasStoppedContainer = true + } + _, err = runtimeService.ContainerStats(cnTruncIndex) + assert.Error(t, err) + assert.NoError(t, runtimeService.RemoveContainer(cnTruncIndex)) + _, err = runtimeService.ContainerStatus(cnTruncIndex) + assert.Error(t, err) + + t.Logf("Get a non exist sandbox status by truncindex") + err = runtimeService.StopPodSandbox(sbTruncIndex) + assert.NoError(t, err) + if err == nil { + hasStoppedSandbox = true + } + assert.NoError(t, runtimeService.RemovePodSandbox(sbTruncIndex)) + _, err = runtimeService.PodSandboxStatus(sbTruncIndex) + assert.Error(t, err) +} diff --git a/integration/util/boottime_util_darwin.go b/integration/util/boottime_util_darwin.go new file mode 100644 index 000000000..74abe9fb9 --- /dev/null +++ b/integration/util/boottime_util_darwin.go @@ -0,0 +1,60 @@ +// +build darwin + +/* + Copyright The containerd 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 + + http://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. +*/ + +/* +Copyright 2018 The Kubernetes 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 + + http://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. +*/ + +package util + +import ( + "fmt" + "syscall" + "time" + "unsafe" + + "golang.org/x/sys/unix" +) + +// GetBootTime returns the time at which the machine was started, truncated to the nearest second +func GetBootTime() (time.Time, error) { + output, err := unix.SysctlRaw("kern.boottime") + if err != nil { + return time.Time{}, err + } + var timeval syscall.Timeval + if len(output) != int(unsafe.Sizeof(timeval)) { + return time.Time{}, fmt.Errorf("unexpected output when calling syscall kern.bootime. Expected len(output) to be %v, but got %v", + int(unsafe.Sizeof(timeval)), len(output)) + } + timeval = *(*syscall.Timeval)(unsafe.Pointer(&output[0])) + sec, nsec := timeval.Unix() + return time.Unix(sec, nsec).Truncate(time.Second), nil +} diff --git a/integration/util/boottime_util_linux.go b/integration/util/boottime_util_linux.go new file mode 100644 index 000000000..2699ae5fe --- /dev/null +++ b/integration/util/boottime_util_linux.go @@ -0,0 +1,52 @@ +// +build freebsd linux + +/* + Copyright The containerd 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 + + http://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. +*/ + +/* +Copyright 2018 The Kubernetes 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 + + http://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. +*/ + +package util + +import ( + "fmt" + "time" + + "golang.org/x/sys/unix" +) + +// GetBootTime returns the time at which the machine was started, truncated to the nearest second +func GetBootTime() (time.Time, error) { + currentTime := time.Now() + var info unix.Sysinfo_t + if err := unix.Sysinfo(&info); err != nil { + return time.Time{}, fmt.Errorf("error getting system uptime: %s", err) + } + return currentTime.Add(-time.Duration(info.Uptime) * time.Second).Truncate(time.Second), nil +} diff --git a/integration/util/doc.go b/integration/util/doc.go new file mode 100644 index 000000000..307fa03c2 --- /dev/null +++ b/integration/util/doc.go @@ -0,0 +1,34 @@ +/* + Copyright The containerd 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 + + http://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. +*/ + +/* +Copyright 2015 The Kubernetes 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 + + http://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. +*/ + +// Package util holds utility functions. +package util diff --git a/integration/util/util.go b/integration/util/util.go new file mode 100644 index 000000000..334cb8b24 --- /dev/null +++ b/integration/util/util.go @@ -0,0 +1,43 @@ +/* + Copyright The containerd 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 + + http://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. +*/ + +/* +Copyright 2017 The Kubernetes 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 + + http://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. +*/ + +package util + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// FromApiserverCache modifies so that the GET request will +// be served from apiserver cache instead of from etcd. +func FromApiserverCache(opts *metav1.GetOptions) { + opts.ResourceVersion = "0" +} diff --git a/integration/util/util_unix.go b/integration/util/util_unix.go new file mode 100644 index 000000000..713df4b63 --- /dev/null +++ b/integration/util/util_unix.go @@ -0,0 +1,170 @@ +// +build freebsd linux darwin + +/* + Copyright The containerd 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 + + http://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. +*/ + +/* +Copyright 2017 The Kubernetes 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 + + http://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. +*/ + +package util + +import ( + "context" + "fmt" + "io/ioutil" + "net" + "net/url" + "os" + "path/filepath" + + "golang.org/x/sys/unix" + "k8s.io/klog/v2" +) + +const ( + // unixProtocol is the network protocol of unix socket. + unixProtocol = "unix" +) + +// CreateListener creates a listener on the specified endpoint. +func CreateListener(endpoint string) (net.Listener, error) { + protocol, addr, err := parseEndpointWithFallbackProtocol(endpoint, unixProtocol) + if err != nil { + return nil, err + } + if protocol != unixProtocol { + return nil, fmt.Errorf("only support unix socket endpoint") + } + + // Unlink to cleanup the previous socket file. + err = unix.Unlink(addr) + if err != nil && !os.IsNotExist(err) { + return nil, fmt.Errorf("failed to unlink socket file %q: %v", addr, err) + } + + if err := os.MkdirAll(filepath.Dir(addr), 0750); err != nil { + return nil, fmt.Errorf("error creating socket directory %q: %v", filepath.Dir(addr), err) + } + + // Create the socket on a tempfile and move it to the destination socket to handle improprer cleanup + file, err := ioutil.TempFile(filepath.Dir(addr), "") + if err != nil { + return nil, fmt.Errorf("failed to create temporary file: %v", err) + } + + if err := os.Remove(file.Name()); err != nil { + return nil, fmt.Errorf("failed to remove temporary file: %v", err) + } + + l, err := net.Listen(protocol, file.Name()) + if err != nil { + return nil, err + } + + if err = os.Rename(file.Name(), addr); err != nil { + return nil, fmt.Errorf("failed to move temporary file to addr %q: %v", addr, err) + } + + return l, nil +} + +// GetAddressAndDialer returns the address parsed from the given endpoint and a context dialer. +func GetAddressAndDialer(endpoint string) (string, func(ctx context.Context, addr string) (net.Conn, error), error) { + protocol, addr, err := parseEndpointWithFallbackProtocol(endpoint, unixProtocol) + if err != nil { + return "", nil, err + } + if protocol != unixProtocol { + return "", nil, fmt.Errorf("only support unix socket endpoint") + } + + return addr, dial, nil +} + +func dial(ctx context.Context, addr string) (net.Conn, error) { + return (&net.Dialer{}).DialContext(ctx, unixProtocol, addr) +} + +func parseEndpointWithFallbackProtocol(endpoint string, fallbackProtocol string) (protocol string, addr string, err error) { + if protocol, addr, err = parseEndpoint(endpoint); err != nil && protocol == "" { + fallbackEndpoint := fallbackProtocol + "://" + endpoint + protocol, addr, err = parseEndpoint(fallbackEndpoint) + if err == nil { + klog.Warningf("Using %q as endpoint is deprecated, please consider using full url format %q.", endpoint, fallbackEndpoint) + } + } + return +} + +func parseEndpoint(endpoint string) (string, string, error) { + u, err := url.Parse(endpoint) + if err != nil { + return "", "", err + } + + switch u.Scheme { + case "tcp": + return "tcp", u.Host, nil + + case "unix": + return "unix", u.Path, nil + + case "": + return "", "", fmt.Errorf("using %q as endpoint is deprecated, please consider using full url format", endpoint) + + default: + return u.Scheme, "", fmt.Errorf("protocol %q not supported", u.Scheme) + } +} + +// LocalEndpoint returns the full path to a unix socket at the given endpoint +func LocalEndpoint(path, file string) (string, error) { + u := url.URL{ + Scheme: unixProtocol, + Path: path, + } + return filepath.Join(u.String(), file+".sock"), nil +} + +// IsUnixDomainSocket returns whether a given file is a AF_UNIX socket file +func IsUnixDomainSocket(filePath string) (bool, error) { + fi, err := os.Stat(filePath) + if err != nil { + return false, fmt.Errorf("stat file %s failed: %v", filePath, err) + } + if fi.Mode()&os.ModeSocket == 0 { + return false, nil + } + return true, nil +} + +// NormalizePath is a no-op for Linux for now +func NormalizePath(path string) string { + return path +} diff --git a/integration/util/util_unsupported.go b/integration/util/util_unsupported.go new file mode 100644 index 000000000..81f412172 --- /dev/null +++ b/integration/util/util_unsupported.go @@ -0,0 +1,71 @@ +// +build !freebsd,!linux,!windows,!darwin + +/* + Copyright The containerd 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 + + http://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. +*/ + +/* +Copyright 2017 The Kubernetes 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 + + http://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. +*/ + +package util + +import ( + "context" + "fmt" + "net" + "time" +) + +// CreateListener creates a listener on the specified endpoint. +func CreateListener(endpoint string) (net.Listener, error) { + return nil, fmt.Errorf("CreateListener is unsupported in this build") +} + +// GetAddressAndDialer returns the address parsed from the given endpoint and a context dialer. +func GetAddressAndDialer(endpoint string) (string, func(ctx context.Context, addr string) (net.Conn, error), error) { + return "", nil, fmt.Errorf("GetAddressAndDialer is unsupported in this build") +} + +// LockAndCheckSubPath empty implementation +func LockAndCheckSubPath(volumePath, subPath string) ([]uintptr, error) { + return []uintptr{}, nil +} + +// UnlockPath empty implementation +func UnlockPath(fileHandles []uintptr) { +} + +// LocalEndpoint empty implementation +func LocalEndpoint(path, file string) (string, error) { + return "", fmt.Errorf("LocalEndpoints are unsupported in this build") +} + +// GetBootTime empty implementation +func GetBootTime() (time.Time, error) { + return time.Time{}, fmt.Errorf("GetBootTime is unsupported in this build") +} diff --git a/integration/util/util_windows.go b/integration/util/util_windows.go new file mode 100644 index 000000000..850a50f81 --- /dev/null +++ b/integration/util/util_windows.go @@ -0,0 +1,170 @@ +// +build windows + +/* + Copyright The containerd 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 + + http://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. +*/ + +/* +Copyright 2017 The Kubernetes 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 + + http://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. +*/ + +package util + +import ( + "context" + "fmt" + "net" + "net/url" + "strings" + "syscall" + "time" + + "github.com/Microsoft/go-winio" +) + +const ( + tcpProtocol = "tcp" + npipeProtocol = "npipe" +) + +// CreateListener creates a listener on the specified endpoint. +func CreateListener(endpoint string) (net.Listener, error) { + protocol, addr, err := parseEndpoint(endpoint) + if err != nil { + return nil, err + } + + switch protocol { + case tcpProtocol: + return net.Listen(tcpProtocol, addr) + + case npipeProtocol: + return winio.ListenPipe(addr, nil) + + default: + return nil, fmt.Errorf("only support tcp and npipe endpoint") + } +} + +// GetAddressAndDialer returns the address parsed from the given endpoint and a context dialer. +func GetAddressAndDialer(endpoint string) (string, func(ctx context.Context, addr string) (net.Conn, error), error) { + protocol, addr, err := parseEndpoint(endpoint) + if err != nil { + return "", nil, err + } + + if protocol == tcpProtocol { + return addr, tcpDial, nil + } + + if protocol == npipeProtocol { + return addr, npipeDial, nil + } + + return "", nil, fmt.Errorf("only support tcp and npipe endpoint") +} + +func tcpDial(ctx context.Context, addr string) (net.Conn, error) { + return (&net.Dialer{}).DialContext(ctx, tcpProtocol, addr) +} + +func npipeDial(ctx context.Context, addr string) (net.Conn, error) { + return winio.DialPipeContext(ctx, addr) +} + +func parseEndpoint(endpoint string) (string, string, error) { + // url.Parse doesn't recognize \, so replace with / first. + endpoint = strings.Replace(endpoint, "\\", "/", -1) + u, err := url.Parse(endpoint) + if err != nil { + return "", "", err + } + + if u.Scheme == "tcp" { + return "tcp", u.Host, nil + } else if u.Scheme == "npipe" { + if strings.HasPrefix(u.Path, "//./pipe") { + return "npipe", u.Path, nil + } + + // fallback host if not provided. + host := u.Host + if host == "" { + host = "." + } + return "npipe", fmt.Sprintf("//%s%s", host, u.Path), nil + } else if u.Scheme == "" { + return "", "", fmt.Errorf("Using %q as endpoint is deprecated, please consider using full url format", endpoint) + } else { + return u.Scheme, "", fmt.Errorf("protocol %q not supported", u.Scheme) + } +} + +// LocalEndpoint empty implementation +func LocalEndpoint(path, file string) (string, error) { + return "", fmt.Errorf("LocalEndpoints are unsupported in this build") +} + +var tickCount = syscall.NewLazyDLL("kernel32.dll").NewProc("GetTickCount64") + +// GetBootTime returns the time at which the machine was started, truncated to the nearest second +func GetBootTime() (time.Time, error) { + currentTime := time.Now() + output, _, err := tickCount.Call() + if errno, ok := err.(syscall.Errno); !ok || errno != 0 { + return time.Time{}, err + } + return currentTime.Add(-time.Duration(output) * time.Millisecond).Truncate(time.Second), nil +} + +// IsUnixDomainSocket returns whether a given file is a AF_UNIX socket file +func IsUnixDomainSocket(filePath string) (bool, error) { + // Due to the absence of golang support for os.ModeSocket in Windows (https://github.com/golang/go/issues/33357) + // we need to dial the file and check if we receive an error to determine if a file is Unix Domain Socket file. + + // Note that querrying for the Reparse Points (https://docs.microsoft.com/en-us/windows/win32/fileio/reparse-points) + // for the file (using FSCTL_GET_REPARSE_POINT) and checking for reparse tag: reparseTagSocket + // does NOT work in 1809 if the socket file is created within a bind mounted directory by a container + // and the FSCTL is issued in the host by the kubelet. + + c, err := net.Dial("unix", filePath) + if err == nil { + c.Close() + return true, nil + } + return false, nil +} + +// NormalizePath converts FS paths returned by certain go frameworks (like fsnotify) +// to native Windows paths that can be passed to Windows specific code +func NormalizePath(path string) string { + path = strings.ReplaceAll(path, "/", "\\") + if strings.HasPrefix(path, "\\") { + path = "c:" + path + } + return path +} diff --git a/integration/volume_copy_up_test.go b/integration/volume_copy_up_test.go new file mode 100644 index 000000000..d9b9f1533 --- /dev/null +++ b/integration/volume_copy_up_test.go @@ -0,0 +1,140 @@ +// +build linux + +/* + Copyright The containerd 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 + + http://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. +*/ + +package integration + +import ( + "fmt" + "os/exec" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" +) + +func TestVolumeCopyUp(t *testing.T) { + const ( + testImage = "gcr.io/k8s-cri-containerd/volume-copy-up:1.0" + execTimeout = time.Minute + ) + + t.Logf("Create a sandbox") + sbConfig := PodSandboxConfig("sandbox", "volume-copy-up") + sb, err := runtimeService.RunPodSandbox(sbConfig, *runtimeHandler) + require.NoError(t, err) + defer func() { + assert.NoError(t, runtimeService.StopPodSandbox(sb)) + assert.NoError(t, runtimeService.RemovePodSandbox(sb)) + }() + + t.Logf("Pull test image") + _, err = imageService.PullImage(&runtime.ImageSpec{Image: testImage}, nil, sbConfig) + require.NoError(t, err) + + t.Logf("Create a container with volume-copy-up test image") + cnConfig := ContainerConfig( + "container", + testImage, + WithCommand("tail", "-f", "/dev/null"), + ) + cn, err := runtimeService.CreateContainer(sb, cnConfig, sbConfig) + require.NoError(t, err) + + t.Logf("Start the container") + require.NoError(t, runtimeService.StartContainer(cn)) + + // gcr.io/k8s-cri-containerd/volume-copy-up:1.0 contains a test_dir + // volume, which contains a test_file with content "test_content". + t.Logf("Check whether volume contains the test file") + stdout, stderr, err := runtimeService.ExecSync(cn, []string{ + "cat", + "/test_dir/test_file", + }, execTimeout) + require.NoError(t, err) + assert.Empty(t, stderr) + assert.Equal(t, "test_content\n", string(stdout)) + + t.Logf("Check host path of the volume") + hostCmd := fmt.Sprintf("find %s/containers/%s/volumes/*/test_file | xargs cat", *criRoot, cn) + output, err := exec.Command("sh", "-c", hostCmd).CombinedOutput() + require.NoError(t, err) + assert.Equal(t, "test_content\n", string(output)) + + t.Logf("Update volume from inside the container") + _, _, err = runtimeService.ExecSync(cn, []string{ + "sh", + "-c", + "echo new_content > /test_dir/test_file", + }, execTimeout) + require.NoError(t, err) + + t.Logf("Check whether host path of the volume is updated") + output, err = exec.Command("sh", "-c", hostCmd).CombinedOutput() + require.NoError(t, err) + assert.Equal(t, "new_content\n", string(output)) +} + +func TestVolumeOwnership(t *testing.T) { + const ( + testImage = "gcr.io/k8s-cri-containerd/volume-ownership:1.0" + execTimeout = time.Minute + ) + + t.Logf("Create a sandbox") + sbConfig := PodSandboxConfig("sandbox", "volume-ownership") + sb, err := runtimeService.RunPodSandbox(sbConfig, *runtimeHandler) + require.NoError(t, err) + defer func() { + assert.NoError(t, runtimeService.StopPodSandbox(sb)) + assert.NoError(t, runtimeService.RemovePodSandbox(sb)) + }() + + t.Logf("Pull test image") + _, err = imageService.PullImage(&runtime.ImageSpec{Image: testImage}, nil, sbConfig) + require.NoError(t, err) + + t.Logf("Create a container with volume-ownership test image") + cnConfig := ContainerConfig( + "container", + testImage, + WithCommand("tail", "-f", "/dev/null"), + ) + cn, err := runtimeService.CreateContainer(sb, cnConfig, sbConfig) + require.NoError(t, err) + + t.Logf("Start the container") + require.NoError(t, runtimeService.StartContainer(cn)) + + // gcr.io/k8s-cri-containerd/volume-ownership:1.0 contains a test_dir + // volume, which is owned by nobody:nogroup. + t.Logf("Check ownership of test directory inside container") + stdout, stderr, err := runtimeService.ExecSync(cn, []string{ + "stat", "-c", "%U:%G", "/test_dir", + }, execTimeout) + require.NoError(t, err) + assert.Empty(t, stderr) + assert.Equal(t, "nobody:nogroup\n", string(stdout)) + + t.Logf("Check ownership of test directory on the host") + hostCmd := fmt.Sprintf("find %s/containers/%s/volumes/* | xargs stat -c %%U:%%G", *criRoot, cn) + output, err := exec.Command("sh", "-c", hostCmd).CombinedOutput() + require.NoError(t, err) + assert.Equal(t, "nobody:nogroup\n", string(output)) +} diff --git a/vendor/github.com/containerd/cri/pkg/annotations/annotations.go b/pkg/annotations/annotations.go similarity index 100% rename from vendor/github.com/containerd/cri/pkg/annotations/annotations.go rename to pkg/annotations/annotations.go diff --git a/vendor/github.com/containerd/cri/pkg/api/runtimeoptions/v1/api.pb.go b/pkg/api/runtimeoptions/v1/api.pb.go similarity index 59% rename from vendor/github.com/containerd/cri/pkg/api/runtimeoptions/v1/api.pb.go rename to pkg/api/runtimeoptions/v1/api.pb.go index bf0cf3d41..63e33f475 100644 --- a/vendor/github.com/containerd/cri/pkg/api/runtimeoptions/v1/api.pb.go +++ b/pkg/api/runtimeoptions/v1/api.pb.go @@ -1,41 +1,18 @@ -/* - Copyright The containerd 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 - - http://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. -*/ // Code generated by protoc-gen-gogo. DO NOT EDIT. -// source: api.proto +// source: github.com/containerd/containerd/pkg/api/runtimeoptions/v1/api.proto -/* - Package cri_runtimeoptions_v1 is a generated protocol buffer package. - - It is generated from these files: - api.proto - - It has these top-level messages: - Options -*/ package cri_runtimeoptions_v1 -import proto "github.com/gogo/protobuf/proto" -import fmt "fmt" -import math "math" -import _ "github.com/gogo/protobuf/gogoproto" - -import strings "strings" -import reflect "reflect" - -import io "io" +import ( + fmt "fmt" + _ "github.com/gogo/protobuf/gogoproto" + proto "github.com/gogo/protobuf/proto" + io "io" + math "math" + math_bits "math/bits" + reflect "reflect" + strings "strings" +) // Reference imports to suppress errors if they are not otherwise used. var _ = proto.Marshal @@ -46,19 +23,49 @@ var _ = math.Inf // is compatible with the proto package it is being compiled against. // A compilation error at this line likely means your copy of the // proto package needs to be updated. -const _ = proto.GoGoProtoPackageIsVersion2 // please upgrade the proto package +const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package type Options struct { // TypeUrl specifies the type of the content inside the config file. TypeUrl string `protobuf:"bytes,1,opt,name=type_url,json=typeUrl,proto3" json:"type_url,omitempty"` // ConfigPath specifies the filesystem location of the config file // used by the runtime. - ConfigPath string `protobuf:"bytes,2,opt,name=config_path,json=configPath,proto3" json:"config_path,omitempty"` + ConfigPath string `protobuf:"bytes,2,opt,name=config_path,json=configPath,proto3" json:"config_path,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_sizecache int32 `json:"-"` } -func (m *Options) Reset() { *m = Options{} } -func (*Options) ProtoMessage() {} -func (*Options) Descriptor() ([]byte, []int) { return fileDescriptorApi, []int{0} } +func (m *Options) Reset() { *m = Options{} } +func (*Options) ProtoMessage() {} +func (*Options) Descriptor() ([]byte, []int) { + return fileDescriptor_8398b2d76ed13c1c, []int{0} +} +func (m *Options) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *Options) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_Options.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *Options) XXX_Merge(src proto.Message) { + xxx_messageInfo_Options.Merge(m, src) +} +func (m *Options) XXX_Size() int { + return m.Size() +} +func (m *Options) XXX_DiscardUnknown() { + xxx_messageInfo_Options.DiscardUnknown(m) +} + +var xxx_messageInfo_Options proto.InternalMessageInfo func (m *Options) GetTypeUrl() string { if m != nil { @@ -77,10 +84,33 @@ func (m *Options) GetConfigPath() string { func init() { proto.RegisterType((*Options)(nil), "cri.runtimeoptions.v1.Options") } + +func init() { + proto.RegisterFile("github.com/containerd/containerd/pkg/api/runtimeoptions/v1/api.proto", fileDescriptor_8398b2d76ed13c1c) +} + +var fileDescriptor_8398b2d76ed13c1c = []byte{ + // 224 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x72, 0x49, 0xcf, 0x2c, 0xc9, + 0x28, 0x4d, 0xd2, 0x4b, 0xce, 0xcf, 0xd5, 0x4f, 0xce, 0xcf, 0x2b, 0x49, 0xcc, 0xcc, 0x4b, 0x2d, + 0x4a, 0x41, 0x66, 0x16, 0x64, 0xa7, 0xeb, 0x27, 0x16, 0x64, 0xea, 0x17, 0x95, 0xe6, 0x95, 0x64, + 0xe6, 0xa6, 0xe6, 0x17, 0x94, 0x64, 0xe6, 0xe7, 0x15, 0xeb, 0x97, 0x19, 0x82, 0x44, 0xf5, 0x0a, + 0x8a, 0xf2, 0x4b, 0xf2, 0x85, 0x44, 0x93, 0x8b, 0x32, 0xf5, 0x50, 0x15, 0xe8, 0x95, 0x19, 0x4a, + 0xe9, 0x22, 0x19, 0x9e, 0x9e, 0x9f, 0x9e, 0xaf, 0x0f, 0x56, 0x9d, 0x54, 0x9a, 0x06, 0xe6, 0x81, + 0x39, 0x60, 0x16, 0xc4, 0x14, 0x25, 0x57, 0x2e, 0x76, 0x7f, 0x88, 0x66, 0x21, 0x49, 0x2e, 0x8e, + 0x92, 0xca, 0x82, 0xd4, 0xf8, 0xd2, 0xa2, 0x1c, 0x09, 0x46, 0x05, 0x46, 0x0d, 0xce, 0x20, 0x76, + 0x10, 0x3f, 0xb4, 0x28, 0x47, 0x48, 0x9e, 0x8b, 0x3b, 0x39, 0x3f, 0x2f, 0x2d, 0x33, 0x3d, 0xbe, + 0x20, 0xb1, 0x24, 0x43, 0x82, 0x09, 0x2c, 0xcb, 0x05, 0x11, 0x0a, 0x48, 0x2c, 0xc9, 0x70, 0xca, + 0x3b, 0xf1, 0x50, 0x8e, 0xf1, 0xc6, 0x43, 0x39, 0x86, 0x86, 0x47, 0x72, 0x8c, 0x27, 0x1e, 0xc9, + 0x31, 0x5e, 0x78, 0x24, 0xc7, 0xf8, 0xe0, 0x91, 0x1c, 0xe3, 0x84, 0xc7, 0x72, 0x0c, 0x51, 0x01, + 0xe4, 0x7b, 0xd8, 0x3a, 0xb9, 0x28, 0x33, 0x1e, 0x55, 0x34, 0xbe, 0xcc, 0x30, 0x89, 0x0d, 0xec, + 0x7a, 0x63, 0x40, 0x00, 0x00, 0x00, 0xff, 0xff, 0xa5, 0x97, 0xee, 0x94, 0x4b, 0x01, 0x00, 0x00, +} + func (m *Options) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) - n, err := m.MarshalTo(dAtA) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) if err != nil { return nil, err } @@ -88,35 +118,47 @@ func (m *Options) Marshal() (dAtA []byte, err error) { } func (m *Options) MarshalTo(dAtA []byte) (int, error) { - var i int + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *Options) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) _ = i var l int _ = l - if len(m.TypeUrl) > 0 { - dAtA[i] = 0xa - i++ - i = encodeVarintApi(dAtA, i, uint64(len(m.TypeUrl))) - i += copy(dAtA[i:], m.TypeUrl) - } if len(m.ConfigPath) > 0 { - dAtA[i] = 0x12 - i++ + i -= len(m.ConfigPath) + copy(dAtA[i:], m.ConfigPath) i = encodeVarintApi(dAtA, i, uint64(len(m.ConfigPath))) - i += copy(dAtA[i:], m.ConfigPath) + i-- + dAtA[i] = 0x12 } - return i, nil + if len(m.TypeUrl) > 0 { + i -= len(m.TypeUrl) + copy(dAtA[i:], m.TypeUrl) + i = encodeVarintApi(dAtA, i, uint64(len(m.TypeUrl))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil } func encodeVarintApi(dAtA []byte, offset int, v uint64) int { + offset -= sovApi(v) + base := offset for v >= 1<<7 { dAtA[offset] = uint8(v&0x7f | 0x80) v >>= 7 offset++ } dAtA[offset] = uint8(v) - return offset + 1 + return base } func (m *Options) Size() (n int) { + if m == nil { + return 0 + } var l int _ = l l = len(m.TypeUrl) @@ -131,14 +173,7 @@ func (m *Options) Size() (n int) { } func sovApi(x uint64) (n int) { - for { - n++ - x >>= 7 - if x == 0 { - break - } - } - return n + return (math_bits.Len64(x|1) + 6) / 7 } func sozApi(x uint64) (n int) { return sovApi(uint64((x << 1) ^ uint64((int64(x) >> 63)))) @@ -177,7 +212,7 @@ func (m *Options) Unmarshal(dAtA []byte) error { } b := dAtA[iNdEx] iNdEx++ - wire |= (uint64(b) & 0x7F) << shift + wire |= uint64(b&0x7F) << shift if b < 0x80 { break } @@ -205,7 +240,7 @@ func (m *Options) Unmarshal(dAtA []byte) error { } b := dAtA[iNdEx] iNdEx++ - stringLen |= (uint64(b) & 0x7F) << shift + stringLen |= uint64(b&0x7F) << shift if b < 0x80 { break } @@ -215,6 +250,9 @@ func (m *Options) Unmarshal(dAtA []byte) error { return ErrInvalidLengthApi } postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthApi + } if postIndex > l { return io.ErrUnexpectedEOF } @@ -234,7 +272,7 @@ func (m *Options) Unmarshal(dAtA []byte) error { } b := dAtA[iNdEx] iNdEx++ - stringLen |= (uint64(b) & 0x7F) << shift + stringLen |= uint64(b&0x7F) << shift if b < 0x80 { break } @@ -244,6 +282,9 @@ func (m *Options) Unmarshal(dAtA []byte) error { return ErrInvalidLengthApi } postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthApi + } if postIndex > l { return io.ErrUnexpectedEOF } @@ -258,6 +299,9 @@ func (m *Options) Unmarshal(dAtA []byte) error { if skippy < 0 { return ErrInvalidLengthApi } + if (iNdEx + skippy) < 0 { + return ErrInvalidLengthApi + } if (iNdEx + skippy) > l { return io.ErrUnexpectedEOF } @@ -273,6 +317,7 @@ func (m *Options) Unmarshal(dAtA []byte) error { func skipApi(dAtA []byte) (n int, err error) { l := len(dAtA) iNdEx := 0 + depth := 0 for iNdEx < l { var wire uint64 for shift := uint(0); ; shift += 7 { @@ -304,10 +349,8 @@ func skipApi(dAtA []byte) (n int, err error) { break } } - return iNdEx, nil case 1: iNdEx += 8 - return iNdEx, nil case 2: var length int for shift := uint(0); ; shift += 7 { @@ -324,71 +367,34 @@ func skipApi(dAtA []byte) (n int, err error) { break } } - iNdEx += length if length < 0 { return 0, ErrInvalidLengthApi } - return iNdEx, nil + iNdEx += length case 3: - for { - var innerWire uint64 - var start int = iNdEx - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return 0, ErrIntOverflowApi - } - if iNdEx >= l { - return 0, io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - innerWire |= (uint64(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - innerWireType := int(innerWire & 0x7) - if innerWireType == 4 { - break - } - next, err := skipApi(dAtA[start:]) - if err != nil { - return 0, err - } - iNdEx = start + next - } - return iNdEx, nil + depth++ case 4: - return iNdEx, nil + if depth == 0 { + return 0, ErrUnexpectedEndOfGroupApi + } + depth-- case 5: iNdEx += 4 - return iNdEx, nil default: return 0, fmt.Errorf("proto: illegal wireType %d", wireType) } + if iNdEx < 0 { + return 0, ErrInvalidLengthApi + } + if depth == 0 { + return iNdEx, nil + } } - panic("unreachable") + return 0, io.ErrUnexpectedEOF } var ( - ErrInvalidLengthApi = fmt.Errorf("proto: negative length found during unmarshaling") - ErrIntOverflowApi = fmt.Errorf("proto: integer overflow") + ErrInvalidLengthApi = fmt.Errorf("proto: negative length found during unmarshaling") + ErrIntOverflowApi = fmt.Errorf("proto: integer overflow") + ErrUnexpectedEndOfGroupApi = fmt.Errorf("proto: unexpected end of group") ) - -func init() { proto.RegisterFile("api.proto", fileDescriptorApi) } - -var fileDescriptorApi = []byte{ - // 183 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0x4c, 0x2c, 0xc8, 0xd4, - 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x12, 0x4d, 0x2e, 0xca, 0xd4, 0x2b, 0x2a, 0xcd, 0x2b, 0xc9, - 0xcc, 0x4d, 0xcd, 0x2f, 0x28, 0xc9, 0xcc, 0xcf, 0x2b, 0xd6, 0x2b, 0x33, 0x94, 0xd2, 0x4d, 0xcf, - 0x2c, 0xc9, 0x28, 0x4d, 0xd2, 0x4b, 0xce, 0xcf, 0xd5, 0x4f, 0xcf, 0x4f, 0xcf, 0xd7, 0x07, 0xab, - 0x4e, 0x2a, 0x4d, 0x03, 0xf3, 0xc0, 0x1c, 0x30, 0x0b, 0x62, 0x8a, 0x92, 0x2b, 0x17, 0xbb, 0x3f, - 0x44, 0xb3, 0x90, 0x24, 0x17, 0x47, 0x49, 0x65, 0x41, 0x6a, 0x7c, 0x69, 0x51, 0x8e, 0x04, 0xa3, - 0x02, 0xa3, 0x06, 0x67, 0x10, 0x3b, 0x88, 0x1f, 0x5a, 0x94, 0x23, 0x24, 0xcf, 0xc5, 0x9d, 0x9c, - 0x9f, 0x97, 0x96, 0x99, 0x1e, 0x5f, 0x90, 0x58, 0x92, 0x21, 0xc1, 0x04, 0x96, 0xe5, 0x82, 0x08, - 0x05, 0x24, 0x96, 0x64, 0x38, 0xc9, 0x9c, 0x78, 0x28, 0xc7, 0x78, 0xe3, 0xa1, 0x1c, 0x43, 0xc3, - 0x23, 0x39, 0xc6, 0x13, 0x8f, 0xe4, 0x18, 0x2f, 0x3c, 0x92, 0x63, 0x7c, 0xf0, 0x48, 0x8e, 0x71, - 0xc2, 0x63, 0x39, 0x86, 0x24, 0x36, 0xb0, 0x5d, 0xc6, 0x80, 0x00, 0x00, 0x00, 0xff, 0xff, 0x07, - 0x00, 0xf2, 0x18, 0xbe, 0x00, 0x00, 0x00, -} diff --git a/vendor/github.com/containerd/cri/pkg/api/runtimeoptions/v1/api.proto b/pkg/api/runtimeoptions/v1/api.proto similarity index 60% rename from vendor/github.com/containerd/cri/pkg/api/runtimeoptions/v1/api.proto rename to pkg/api/runtimeoptions/v1/api.proto index f907d609c..e8bcde428 100644 --- a/vendor/github.com/containerd/cri/pkg/api/runtimeoptions/v1/api.proto +++ b/pkg/api/runtimeoptions/v1/api.proto @@ -13,10 +13,13 @@ option (gogoproto.sizer_all) = true; option (gogoproto.unmarshaler_all) = true; option (gogoproto.goproto_unrecognized_all) = false; + +option go_package = "github.com/containerd/containerd/pkg/api/runtimeoptions/v1;cri_runtimeoptions_v1"; + message Options { - // TypeUrl specifies the type of the content inside the config file. - string type_url = 1; - // ConfigPath specifies the filesystem location of the config file - // used by the runtime. - string config_path = 2; + // TypeUrl specifies the type of the content inside the config file. + string type_url = 1; + // ConfigPath specifies the filesystem location of the config file + // used by the runtime. + string config_path = 2; } diff --git a/vendor/github.com/containerd/cri/pkg/atomic/atomic_boolean.go b/pkg/atomic/atomic_boolean.go similarity index 100% rename from vendor/github.com/containerd/cri/pkg/atomic/atomic_boolean.go rename to pkg/atomic/atomic_boolean.go diff --git a/pkg/atomic/atomic_boolean_test.go b/pkg/atomic/atomic_boolean_test.go new file mode 100644 index 000000000..97e5a4b55 --- /dev/null +++ b/pkg/atomic/atomic_boolean_test.go @@ -0,0 +1,32 @@ +/* + Copyright The containerd 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 + + http://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. +*/ + +package atomic + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestBoolean(t *testing.T) { + ab := NewBool(true) + assert.True(t, ab.IsSet()) + ab.Unset() + assert.False(t, ab.IsSet()) + ab.Set() + assert.True(t, ab.IsSet()) +} diff --git a/vendor/github.com/containerd/cri/pkg/config/config.go b/pkg/config/config.go similarity index 100% rename from vendor/github.com/containerd/cri/pkg/config/config.go rename to pkg/config/config.go diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go new file mode 100644 index 000000000..3b544f7b8 --- /dev/null +++ b/pkg/config/config_test.go @@ -0,0 +1,334 @@ +/* + Copyright The containerd 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 + + http://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. +*/ + +package config + +import ( + "context" + "fmt" + "testing" + + "github.com/containerd/containerd/plugin" + "github.com/stretchr/testify/assert" +) + +func TestValidateConfig(t *testing.T) { + for desc, test := range map[string]struct { + config *PluginConfig + expectedErr string + expected *PluginConfig + }{ + "deprecated untrusted_workload_runtime": { + config: &PluginConfig{ + ContainerdConfig: ContainerdConfig{ + DefaultRuntimeName: RuntimeDefault, + UntrustedWorkloadRuntime: Runtime{ + Type: "untrusted", + }, + Runtimes: map[string]Runtime{ + RuntimeDefault: { + Type: "default", + }, + }, + }, + }, + expected: &PluginConfig{ + ContainerdConfig: ContainerdConfig{ + DefaultRuntimeName: RuntimeDefault, + UntrustedWorkloadRuntime: Runtime{ + Type: "untrusted", + }, + Runtimes: map[string]Runtime{ + RuntimeUntrusted: { + Type: "untrusted", + }, + RuntimeDefault: { + Type: "default", + }, + }, + }, + }, + }, + "both untrusted_workload_runtime and runtime[untrusted]": { + config: &PluginConfig{ + ContainerdConfig: ContainerdConfig{ + DefaultRuntimeName: RuntimeDefault, + UntrustedWorkloadRuntime: Runtime{ + Type: "untrusted-1", + }, + Runtimes: map[string]Runtime{ + RuntimeUntrusted: { + Type: "untrusted-2", + }, + RuntimeDefault: { + Type: "default", + }, + }, + }, + }, + expectedErr: fmt.Sprintf("conflicting definitions: configuration includes both `untrusted_workload_runtime` and `runtimes[%q]`", RuntimeUntrusted), + }, + "deprecated default_runtime": { + config: &PluginConfig{ + ContainerdConfig: ContainerdConfig{ + DefaultRuntime: Runtime{ + Type: "default", + }, + }, + }, + expected: &PluginConfig{ + ContainerdConfig: ContainerdConfig{ + DefaultRuntime: Runtime{ + Type: "default", + }, + DefaultRuntimeName: RuntimeDefault, + Runtimes: map[string]Runtime{ + RuntimeDefault: { + Type: "default", + }, + }, + }, + }, + }, + "no default_runtime_name": { + config: &PluginConfig{}, + expectedErr: "`default_runtime_name` is empty", + }, + "no runtime[default_runtime_name]": { + config: &PluginConfig{ + ContainerdConfig: ContainerdConfig{ + DefaultRuntimeName: RuntimeDefault, + }, + }, + expectedErr: "no corresponding runtime configured in `runtimes` for `default_runtime_name`", + }, + "deprecated systemd_cgroup for v1 runtime": { + config: &PluginConfig{ + SystemdCgroup: true, + ContainerdConfig: ContainerdConfig{ + DefaultRuntimeName: RuntimeDefault, + Runtimes: map[string]Runtime{ + RuntimeDefault: { + Type: plugin.RuntimeLinuxV1, + }, + }, + }, + }, + expected: &PluginConfig{ + SystemdCgroup: true, + ContainerdConfig: ContainerdConfig{ + DefaultRuntimeName: RuntimeDefault, + Runtimes: map[string]Runtime{ + RuntimeDefault: { + Type: plugin.RuntimeLinuxV1, + }, + }, + }, + }, + }, + "deprecated systemd_cgroup for v2 runtime": { + config: &PluginConfig{ + SystemdCgroup: true, + ContainerdConfig: ContainerdConfig{ + DefaultRuntimeName: RuntimeDefault, + Runtimes: map[string]Runtime{ + RuntimeDefault: { + Type: plugin.RuntimeRuncV1, + }, + }, + }, + }, + expectedErr: fmt.Sprintf("`systemd_cgroup` only works for runtime %s", plugin.RuntimeLinuxV1), + }, + "no_pivot for v1 runtime": { + config: &PluginConfig{ + ContainerdConfig: ContainerdConfig{ + NoPivot: true, + DefaultRuntimeName: RuntimeDefault, + Runtimes: map[string]Runtime{ + RuntimeDefault: { + Type: plugin.RuntimeLinuxV1, + }, + }, + }, + }, + expected: &PluginConfig{ + ContainerdConfig: ContainerdConfig{ + NoPivot: true, + DefaultRuntimeName: RuntimeDefault, + Runtimes: map[string]Runtime{ + RuntimeDefault: { + Type: plugin.RuntimeLinuxV1, + }, + }, + }, + }, + }, + "no_pivot for v2 runtime": { + config: &PluginConfig{ + ContainerdConfig: ContainerdConfig{ + NoPivot: true, + DefaultRuntimeName: RuntimeDefault, + Runtimes: map[string]Runtime{ + RuntimeDefault: { + Type: plugin.RuntimeRuncV1, + }, + }, + }, + }, + expectedErr: fmt.Sprintf("`no_pivot` only works for runtime %s", plugin.RuntimeLinuxV1), + }, + "deprecated runtime_engine for v1 runtime": { + config: &PluginConfig{ + ContainerdConfig: ContainerdConfig{ + DefaultRuntimeName: RuntimeDefault, + Runtimes: map[string]Runtime{ + RuntimeDefault: { + Engine: "runc", + Type: plugin.RuntimeLinuxV1, + }, + }, + }, + }, + expected: &PluginConfig{ + ContainerdConfig: ContainerdConfig{ + DefaultRuntimeName: RuntimeDefault, + Runtimes: map[string]Runtime{ + RuntimeDefault: { + Engine: "runc", + Type: plugin.RuntimeLinuxV1, + }, + }, + }, + }, + }, + "deprecated runtime_engine for v2 runtime": { + config: &PluginConfig{ + ContainerdConfig: ContainerdConfig{ + DefaultRuntimeName: RuntimeDefault, + Runtimes: map[string]Runtime{ + RuntimeDefault: { + Engine: "runc", + Type: plugin.RuntimeRuncV1, + }, + }, + }, + }, + expectedErr: fmt.Sprintf("`runtime_engine` only works for runtime %s", plugin.RuntimeLinuxV1), + }, + "deprecated runtime_root for v1 runtime": { + config: &PluginConfig{ + ContainerdConfig: ContainerdConfig{ + DefaultRuntimeName: RuntimeDefault, + Runtimes: map[string]Runtime{ + RuntimeDefault: { + Root: "/run/containerd/runc", + Type: plugin.RuntimeLinuxV1, + }, + }, + }, + }, + expected: &PluginConfig{ + ContainerdConfig: ContainerdConfig{ + DefaultRuntimeName: RuntimeDefault, + Runtimes: map[string]Runtime{ + RuntimeDefault: { + Root: "/run/containerd/runc", + Type: plugin.RuntimeLinuxV1, + }, + }, + }, + }, + }, + "deprecated runtime_root for v2 runtime": { + config: &PluginConfig{ + ContainerdConfig: ContainerdConfig{ + DefaultRuntimeName: RuntimeDefault, + Runtimes: map[string]Runtime{ + RuntimeDefault: { + Root: "/run/containerd/runc", + Type: plugin.RuntimeRuncV1, + }, + }, + }, + }, + expectedErr: fmt.Sprintf("`runtime_root` only works for runtime %s", plugin.RuntimeLinuxV1), + }, + "deprecated auths": { + config: &PluginConfig{ + ContainerdConfig: ContainerdConfig{ + DefaultRuntimeName: RuntimeDefault, + Runtimes: map[string]Runtime{ + RuntimeDefault: { + Type: plugin.RuntimeRuncV1, + }, + }, + }, + Registry: Registry{ + Auths: map[string]AuthConfig{ + "https://gcr.io": {Username: "test"}, + }, + }, + }, + expected: &PluginConfig{ + ContainerdConfig: ContainerdConfig{ + DefaultRuntimeName: RuntimeDefault, + Runtimes: map[string]Runtime{ + RuntimeDefault: { + Type: plugin.RuntimeRuncV1, + }, + }, + }, + Registry: Registry{ + Configs: map[string]RegistryConfig{ + "https://gcr.io": { + Auth: &AuthConfig{ + Username: "test", + }, + }, + }, + Auths: map[string]AuthConfig{ + "https://gcr.io": {Username: "test"}, + }, + }, + }, + }, + "invalid stream_idle_timeout": { + config: &PluginConfig{ + StreamIdleTimeout: "invalid", + ContainerdConfig: ContainerdConfig{ + DefaultRuntimeName: RuntimeDefault, + Runtimes: map[string]Runtime{ + RuntimeDefault: { + Type: "default", + }, + }, + }, + }, + expectedErr: "invalid stream idle timeout", + }, + } { + t.Run(desc, func(t *testing.T) { + err := ValidatePluginConfig(context.Background(), test.config) + if test.expectedErr != "" { + assert.Contains(t, err.Error(), test.expectedErr) + } else { + assert.NoError(t, err) + assert.Equal(t, test.expected, test.config) + } + }) + } +} diff --git a/vendor/github.com/containerd/cri/pkg/config/config_unix.go b/pkg/config/config_unix.go similarity index 97% rename from vendor/github.com/containerd/cri/pkg/config/config_unix.go rename to pkg/config/config_unix.go index 9df456b53..6d8523abb 100644 --- a/vendor/github.com/containerd/cri/pkg/config/config_unix.go +++ b/pkg/config/config_unix.go @@ -21,7 +21,7 @@ package config import ( "github.com/BurntSushi/toml" "github.com/containerd/containerd" - "github.com/containerd/cri/pkg/streaming" + "github.com/containerd/containerd/pkg/streaming" ) // DefaultConfig returns default configurations of cri plugin. diff --git a/vendor/github.com/containerd/cri/pkg/config/config_windows.go b/pkg/config/config_windows.go similarity index 97% rename from vendor/github.com/containerd/cri/pkg/config/config_windows.go rename to pkg/config/config_windows.go index d559b4160..de4593bdb 100644 --- a/vendor/github.com/containerd/cri/pkg/config/config_windows.go +++ b/pkg/config/config_windows.go @@ -23,7 +23,7 @@ import ( "path/filepath" "github.com/containerd/containerd" - "github.com/containerd/cri/pkg/streaming" + "github.com/containerd/containerd/pkg/streaming" ) // DefaultConfig returns default configurations of cri plugin. diff --git a/vendor/github.com/containerd/cri/pkg/constants/constants.go b/pkg/constants/constants.go similarity index 100% rename from vendor/github.com/containerd/cri/pkg/constants/constants.go rename to pkg/constants/constants.go diff --git a/vendor/github.com/containerd/cri/pkg/containerd/opts/container.go b/pkg/containerd/opts/container.go similarity index 100% rename from vendor/github.com/containerd/cri/pkg/containerd/opts/container.go rename to pkg/containerd/opts/container.go diff --git a/vendor/github.com/containerd/cri/pkg/containerd/opts/spec.go b/pkg/containerd/opts/spec.go similarity index 100% rename from vendor/github.com/containerd/cri/pkg/containerd/opts/spec.go rename to pkg/containerd/opts/spec.go diff --git a/vendor/github.com/containerd/cri/pkg/containerd/opts/spec_unix.go b/pkg/containerd/opts/spec_linux.go similarity index 99% rename from vendor/github.com/containerd/cri/pkg/containerd/opts/spec_unix.go rename to pkg/containerd/opts/spec_linux.go index d644962d5..a52c68a3c 100644 --- a/vendor/github.com/containerd/cri/pkg/containerd/opts/spec_unix.go +++ b/pkg/containerd/opts/spec_linux.go @@ -1,5 +1,3 @@ -// +build !windows - /* Copyright The containerd Authors. @@ -42,8 +40,8 @@ import ( "golang.org/x/sys/unix" runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" - osinterface "github.com/containerd/cri/pkg/os" - "github.com/containerd/cri/pkg/util" + osinterface "github.com/containerd/containerd/pkg/os" + "github.com/containerd/containerd/pkg/util" ) // WithAdditionalGIDs adds any additional groups listed for a particular user in the diff --git a/pkg/containerd/opts/spec_linux_test.go b/pkg/containerd/opts/spec_linux_test.go new file mode 100644 index 000000000..1c9942f80 --- /dev/null +++ b/pkg/containerd/opts/spec_linux_test.go @@ -0,0 +1,47 @@ +/* + Copyright The containerd 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 + + http://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. +*/ + +package opts + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestMergeGids(t *testing.T) { + gids1 := []uint32{3, 2, 1} + gids2 := []uint32{2, 3, 4} + assert.Equal(t, []uint32{1, 2, 3, 4}, mergeGids(gids1, gids2)) +} + +func TestRestrictOOMScoreAdj(t *testing.T) { + current, err := getCurrentOOMScoreAdj() + require.NoError(t, err) + + got, err := restrictOOMScoreAdj(current - 1) + require.NoError(t, err) + assert.Equal(t, got, current) + + got, err = restrictOOMScoreAdj(current) + require.NoError(t, err) + assert.Equal(t, got, current) + + got, err = restrictOOMScoreAdj(current + 1) + require.NoError(t, err) + assert.Equal(t, got, current+1) +} diff --git a/pkg/containerd/opts/spec_test.go b/pkg/containerd/opts/spec_test.go new file mode 100644 index 000000000..3e540c3f4 --- /dev/null +++ b/pkg/containerd/opts/spec_test.go @@ -0,0 +1,46 @@ +/* + Copyright The containerd 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 + + http://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. +*/ + +package opts + +import ( + "sort" + "testing" + + "github.com/stretchr/testify/assert" + runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" +) + +func TestOrderedMounts(t *testing.T) { + mounts := []*runtime.Mount{ + {ContainerPath: "/a/b/c"}, + {ContainerPath: "/a/b"}, + {ContainerPath: "/a/b/c/d"}, + {ContainerPath: "/a"}, + {ContainerPath: "/b"}, + {ContainerPath: "/b/c"}, + } + expected := []*runtime.Mount{ + {ContainerPath: "/a"}, + {ContainerPath: "/b"}, + {ContainerPath: "/a/b"}, + {ContainerPath: "/b/c"}, + {ContainerPath: "/a/b/c"}, + {ContainerPath: "/a/b/c/d"}, + } + sort.Stable(orderedMounts(mounts)) + assert.Equal(t, expected, mounts) +} diff --git a/vendor/github.com/containerd/cri/pkg/containerd/opts/spec_windows.go b/pkg/containerd/opts/spec_windows.go similarity index 99% rename from vendor/github.com/containerd/cri/pkg/containerd/opts/spec_windows.go rename to pkg/containerd/opts/spec_windows.go index 50ee19d48..f6d4a1648 100644 --- a/vendor/github.com/containerd/cri/pkg/containerd/opts/spec_windows.go +++ b/pkg/containerd/opts/spec_windows.go @@ -30,7 +30,7 @@ import ( "github.com/pkg/errors" runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" - osinterface "github.com/containerd/cri/pkg/os" + osinterface "github.com/containerd/containerd/pkg/os" ) // WithWindowsNetworkNamespace sets windows network namespace for container. diff --git a/vendor/github.com/containerd/cri/pkg/containerd/opts/task.go b/pkg/containerd/opts/task.go similarity index 100% rename from vendor/github.com/containerd/cri/pkg/containerd/opts/task.go rename to pkg/containerd/opts/task.go diff --git a/vendor/github.com/containerd/cri/pkg/containerd/platforms/default_unix.go b/pkg/containerd/platforms/default_unix.go similarity index 100% rename from vendor/github.com/containerd/cri/pkg/containerd/platforms/default_unix.go rename to pkg/containerd/platforms/default_unix.go diff --git a/vendor/github.com/containerd/cri/pkg/containerd/platforms/default_windows.go b/pkg/containerd/platforms/default_windows.go similarity index 100% rename from vendor/github.com/containerd/cri/pkg/containerd/platforms/default_windows.go rename to pkg/containerd/platforms/default_windows.go diff --git a/pkg/containerd/platforms/default_windows_test.go b/pkg/containerd/platforms/default_windows_test.go new file mode 100644 index 000000000..0f45c97f7 --- /dev/null +++ b/pkg/containerd/platforms/default_windows_test.go @@ -0,0 +1,150 @@ +// +build windows + +/* + Copyright The containerd 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 + + http://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. +*/ + +package platforms + +import ( + "sort" + "testing" + + "github.com/containerd/containerd/platforms" + imagespec "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/stretchr/testify/assert" +) + +func TestMatchComparerMatch(t *testing.T) { + m := matchComparer{ + defaults: platforms.Only(imagespec.Platform{ + Architecture: "amd64", + OS: "windows", + }), + osVersionPrefix: "10.0.17763", + } + for _, test := range []struct { + platform imagespec.Platform + match bool + }{ + { + platform: imagespec.Platform{ + Architecture: "amd64", + OS: "windows", + OSVersion: "10.0.17763.1", + }, + match: true, + }, + { + platform: imagespec.Platform{ + Architecture: "amd64", + OS: "windows", + OSVersion: "10.0.17763.2", + }, + match: true, + }, + { + platform: imagespec.Platform{ + Architecture: "amd64", + OS: "windows", + OSVersion: "10.0.17762.1", + }, + match: false, + }, + { + platform: imagespec.Platform{ + Architecture: "amd64", + OS: "windows", + OSVersion: "10.0.17764.1", + }, + match: false, + }, + { + platform: imagespec.Platform{ + Architecture: "amd64", + OS: "windows", + }, + match: false, + }, + } { + assert.Equal(t, test.match, m.Match(test.platform)) + } +} + +func TestMatchComparerLess(t *testing.T) { + m := matchComparer{ + defaults: platforms.Only(imagespec.Platform{ + Architecture: "amd64", + OS: "windows", + }), + osVersionPrefix: "10.0.17763", + } + platforms := []imagespec.Platform{ + { + Architecture: "amd64", + OS: "windows", + OSVersion: "10.0.17764.1", + }, + { + Architecture: "amd64", + OS: "windows", + }, + { + Architecture: "amd64", + OS: "windows", + OSVersion: "10.0.17763.1", + }, + { + Architecture: "amd64", + OS: "windows", + OSVersion: "10.0.17763.2", + }, + { + Architecture: "amd64", + OS: "windows", + OSVersion: "10.0.17762.1", + }, + } + expected := []imagespec.Platform{ + { + Architecture: "amd64", + OS: "windows", + OSVersion: "10.0.17763.2", + }, + { + Architecture: "amd64", + OS: "windows", + OSVersion: "10.0.17763.1", + }, + { + Architecture: "amd64", + OS: "windows", + OSVersion: "10.0.17764.1", + }, + { + Architecture: "amd64", + OS: "windows", + }, + { + Architecture: "amd64", + OS: "windows", + OSVersion: "10.0.17762.1", + }, + } + sort.SliceStable(platforms, func(i, j int) bool { + return m.Less(platforms[i], platforms[j]) + }) + assert.Equal(t, expected, platforms) +} diff --git a/vendor/github.com/containerd/cri/pkg/containerd/util/util.go b/pkg/containerd/util/util.go similarity index 96% rename from vendor/github.com/containerd/cri/pkg/containerd/util/util.go rename to pkg/containerd/util/util.go index ec062df0d..27f2167af 100644 --- a/vendor/github.com/containerd/cri/pkg/containerd/util/util.go +++ b/pkg/containerd/util/util.go @@ -22,7 +22,7 @@ import ( "github.com/containerd/containerd/namespaces" "golang.org/x/net/context" - "github.com/containerd/cri/pkg/constants" + "github.com/containerd/containerd/pkg/constants" ) // deferCleanupTimeout is the default timeout for containerd cleanup operations diff --git a/vendor/github.com/containerd/cri/cri.go b/pkg/cri/cri.go similarity index 96% rename from vendor/github.com/containerd/cri/cri.go rename to pkg/cri/cri.go index 7d9cc5fc7..6150723fd 100644 --- a/vendor/github.com/containerd/cri/cri.go +++ b/pkg/cri/cri.go @@ -39,10 +39,10 @@ import ( "github.com/sirupsen/logrus" "k8s.io/klog/v2" - criconfig "github.com/containerd/cri/pkg/config" - "github.com/containerd/cri/pkg/constants" - criplatforms "github.com/containerd/cri/pkg/containerd/platforms" - "github.com/containerd/cri/pkg/server" + criconfig "github.com/containerd/containerd/pkg/config" + "github.com/containerd/containerd/pkg/constants" + criplatforms "github.com/containerd/containerd/pkg/containerd/platforms" + "github.com/containerd/containerd/pkg/server" ) // TODO(random-liu): Use github.com/pkg/errors for our errors. diff --git a/vendor/github.com/containerd/cri/pkg/ioutil/read_closer.go b/pkg/ioutil/read_closer.go similarity index 100% rename from vendor/github.com/containerd/cri/pkg/ioutil/read_closer.go rename to pkg/ioutil/read_closer.go diff --git a/pkg/ioutil/read_closer_test.go b/pkg/ioutil/read_closer_test.go new file mode 100644 index 000000000..b77e20ec9 --- /dev/null +++ b/pkg/ioutil/read_closer_test.go @@ -0,0 +1,47 @@ +/* + Copyright The containerd 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 + + http://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. +*/ + +package ioutil + +import ( + "bytes" + "io" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestWrapReadCloser(t *testing.T) { + buf := bytes.NewBufferString("abc") + + rc := NewWrapReadCloser(buf) + dst := make([]byte, 1) + n, err := rc.Read(dst) + assert.Equal(t, 1, n) + assert.NoError(t, err) + assert.Equal(t, []byte("a"), dst) + + n, err = rc.Read(dst) + assert.Equal(t, 1, n) + assert.NoError(t, err) + assert.Equal(t, []byte("b"), dst) + + rc.Close() + n, err = rc.Read(dst) + assert.Equal(t, 0, n) + assert.Equal(t, io.EOF, err) + assert.Equal(t, []byte("b"), dst) +} diff --git a/vendor/github.com/containerd/cri/pkg/ioutil/write_closer.go b/pkg/ioutil/write_closer.go similarity index 100% rename from vendor/github.com/containerd/cri/pkg/ioutil/write_closer.go rename to pkg/ioutil/write_closer.go diff --git a/pkg/ioutil/write_closer_test.go b/pkg/ioutil/write_closer_test.go new file mode 100644 index 000000000..25272a5fd --- /dev/null +++ b/pkg/ioutil/write_closer_test.go @@ -0,0 +1,108 @@ +/* + Copyright The containerd 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 + + http://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. +*/ + +package ioutil + +import ( + "io/ioutil" + "os" + "sort" + "strconv" + "strings" + "sync" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestWriteCloseInformer(t *testing.T) { + original := &writeCloser{} + wci, close := NewWriteCloseInformer(original) + data := "test" + + n, err := wci.Write([]byte(data)) + assert.Equal(t, len(data), n) + assert.Equal(t, data, original.buf.String()) + assert.NoError(t, err) + + select { + case <-close: + assert.Fail(t, "write closer closed") + default: + } + + wci.Close() + assert.True(t, original.closed) + + select { + case <-close: + default: + assert.Fail(t, "write closer not closed") + } +} + +func TestSerialWriteCloser(t *testing.T) { + const ( + // Test 10 times to make sure it always pass. + testCount = 10 + + goroutine = 10 + dataLen = 100000 + ) + for n := 0; n < testCount; n++ { + testData := make([][]byte, goroutine) + for i := 0; i < goroutine; i++ { + testData[i] = []byte(repeatNumber(i, dataLen) + "\n") + } + + f, err := ioutil.TempFile("", "serial-write-closer") + require.NoError(t, err) + defer os.RemoveAll(f.Name()) + defer f.Close() + wc := NewSerialWriteCloser(f) + defer wc.Close() + + // Write data in parallel + var wg sync.WaitGroup + wg.Add(goroutine) + for i := 0; i < goroutine; i++ { + go func(id int) { + n, err := wc.Write(testData[id]) + assert.NoError(t, err) + assert.Equal(t, dataLen+1, n) + wg.Done() + }(i) + } + wg.Wait() + wc.Close() + + // Check test result + content, err := ioutil.ReadFile(f.Name()) + require.NoError(t, err) + resultData := strings.Split(strings.TrimSpace(string(content)), "\n") + require.Len(t, resultData, goroutine) + sort.Strings(resultData) + for i := 0; i < goroutine; i++ { + expected := repeatNumber(i, dataLen) + assert.Equal(t, expected, resultData[i]) + } + } +} + +func repeatNumber(num, count int) string { + return strings.Repeat(strconv.Itoa(num), count) +} diff --git a/vendor/github.com/containerd/cri/pkg/ioutil/writer_group.go b/pkg/ioutil/writer_group.go similarity index 100% rename from vendor/github.com/containerd/cri/pkg/ioutil/writer_group.go rename to pkg/ioutil/writer_group.go diff --git a/pkg/ioutil/writer_group_test.go b/pkg/ioutil/writer_group_test.go new file mode 100644 index 000000000..289ea25fa --- /dev/null +++ b/pkg/ioutil/writer_group_test.go @@ -0,0 +1,115 @@ +/* + Copyright The containerd 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 + + http://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. +*/ + +package ioutil + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" +) + +type writeCloser struct { + buf bytes.Buffer + closed bool +} + +func (wc *writeCloser) Write(p []byte) (int, error) { + return wc.buf.Write(p) +} + +func (wc *writeCloser) Close() error { + wc.closed = true + return nil +} + +func TestEmptyWriterGroup(t *testing.T) { + wg := NewWriterGroup() + _, err := wg.Write([]byte("test")) + assert.Error(t, err) +} + +func TestClosedWriterGroup(t *testing.T) { + wg := NewWriterGroup() + wc := &writeCloser{} + key, data := "test key", "test data" + + wg.Add(key, wc) + + n, err := wg.Write([]byte(data)) + assert.Equal(t, len(data), n) + assert.Equal(t, data, wc.buf.String()) + assert.NoError(t, err) + + wg.Close() + assert.True(t, wc.closed) + + newWC := &writeCloser{} + wg.Add(key, newWC) + assert.True(t, newWC.closed) + + _, err = wg.Write([]byte(data)) + assert.Error(t, err) +} + +func TestAddGetRemoveWriter(t *testing.T) { + wg := NewWriterGroup() + wc1, wc2 := &writeCloser{}, &writeCloser{} + key1, key2 := "test key 1", "test key 2" + + wg.Add(key1, wc1) + _, err := wg.Write([]byte("test data 1")) + assert.NoError(t, err) + assert.Equal(t, "test data 1", wc1.buf.String()) + + wg.Add(key2, wc2) + _, err = wg.Write([]byte("test data 2")) + assert.NoError(t, err) + assert.Equal(t, "test data 1test data 2", wc1.buf.String()) + assert.Equal(t, "test data 2", wc2.buf.String()) + + assert.Equal(t, wc1, wg.Get(key1)) + + wg.Remove(key1) + _, err = wg.Write([]byte("test data 3")) + assert.NoError(t, err) + assert.Equal(t, "test data 1test data 2", wc1.buf.String()) + assert.Equal(t, "test data 2test data 3", wc2.buf.String()) + + assert.Equal(t, nil, wg.Get(key1)) + + wg.Close() +} + +func TestReplaceWriter(t *testing.T) { + wg := NewWriterGroup() + wc1, wc2 := &writeCloser{}, &writeCloser{} + key := "test-key" + + wg.Add(key, wc1) + _, err := wg.Write([]byte("test data 1")) + assert.NoError(t, err) + assert.Equal(t, "test data 1", wc1.buf.String()) + + wg.Add(key, wc2) + _, err = wg.Write([]byte("test data 2")) + assert.NoError(t, err) + assert.Equal(t, "test data 1", wc1.buf.String()) + assert.Equal(t, "test data 2", wc2.buf.String()) + + wg.Close() +} diff --git a/vendor/github.com/containerd/cri/pkg/netns/netns_unix.go b/pkg/netns/netns_linux.go similarity index 97% rename from vendor/github.com/containerd/cri/pkg/netns/netns_unix.go rename to pkg/netns/netns_linux.go index 7449e2350..ff879d9d6 100644 --- a/vendor/github.com/containerd/cri/pkg/netns/netns_unix.go +++ b/pkg/netns/netns_linux.go @@ -1,5 +1,3 @@ -// +build !windows - /* Copyright The containerd Authors. @@ -25,6 +23,7 @@ // http://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 @@ -40,12 +39,11 @@ import ( "runtime" "sync" + "github.com/containerd/containerd/mount" cnins "github.com/containernetworking/plugins/pkg/ns" "github.com/docker/docker/pkg/symlink" "github.com/pkg/errors" "golang.org/x/sys/unix" - - osinterface "github.com/containerd/cri/pkg/os" ) const nsRunDir = "/var/run/netns" @@ -141,7 +139,7 @@ func unmountNS(path string) error { if err != nil { return errors.Wrap(err, "failed to follow symlink") } - if err := osinterface.Unmount(path); err != nil && !os.IsNotExist(err) { + if err := mount.Unmount(path, unix.MNT_DETACH); err != nil && !os.IsNotExist(err) { return errors.Wrap(err, "failed to umount netns") } if err := os.RemoveAll(path); err != nil { diff --git a/pkg/netns/netns_other.go b/pkg/netns/netns_other.go new file mode 100644 index 000000000..253987ea6 --- /dev/null +++ b/pkg/netns/netns_other.go @@ -0,0 +1,58 @@ +// +build !windows,!linux + +/* + Copyright The containerd 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 + + http://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. +*/ + +package netns + +import ( + "github.com/pkg/errors" +) + +var errNotImplementedOnUnix = errors.New("not implemented on unix") + +// NetNS holds network namespace. +type NetNS struct { + path string +} + +// NewNetNS creates a network namespace. +func NewNetNS() (*NetNS, error) { + return nil, errNotImplementedOnUnix +} + +// LoadNetNS loads existing network namespace. +func LoadNetNS(path string) *NetNS { + return &NetNS{path: path} +} + +// Remove removes network namepace. Remove is idempotent, meaning it might +// be invoked multiple times and provides consistent result. +func (n *NetNS) Remove() error { + return errNotImplementedOnUnix +} + +// Closed checks whether the network namespace has been closed. +func (n *NetNS) Closed() (bool, error) { + return false, errNotImplementedOnUnix +} + +// GetPath returns network namespace path for sandbox container +func (n *NetNS) GetPath() string { + return n.path +} + +// NOTE: Do function is not supported. diff --git a/vendor/github.com/containerd/cri/pkg/netns/netns_windows.go b/pkg/netns/netns_windows.go similarity index 100% rename from vendor/github.com/containerd/cri/pkg/netns/netns_windows.go rename to pkg/netns/netns_windows.go diff --git a/vendor/github.com/containerd/cri/pkg/os/os_unix.go b/pkg/os/mount_linux.go similarity index 63% rename from vendor/github.com/containerd/cri/pkg/os/os_unix.go rename to pkg/os/mount_linux.go index 51f57871d..15228e57d 100644 --- a/vendor/github.com/containerd/cri/pkg/os/os_unix.go +++ b/pkg/os/mount_linux.go @@ -1,5 +1,3 @@ -// +build !windows - /* Copyright The containerd Authors. @@ -23,14 +21,6 @@ import ( "golang.org/x/sys/unix" ) -// UNIX collects unix system level operations that need to be -// mocked out during tests. -type UNIX interface { - Mount(source string, target string, fstype string, flags uintptr, data string) error - Unmount(target string) error - LookupMount(path string) (mount.Info, error) -} - // Mount will call unix.Mount to mount the file. func (RealOS) Mount(source string, target string, fstype string, flags uintptr, data string) error { return unix.Mount(source, target, fstype, flags, data) @@ -38,22 +28,10 @@ func (RealOS) Mount(source string, target string, fstype string, flags uintptr, // Unmount will call Unmount to unmount the file. func (RealOS) Unmount(target string) error { - return Unmount(target) + return mount.Unmount(target, unix.MNT_DETACH) } // LookupMount gets mount info of a given path. func (RealOS) LookupMount(path string) (mount.Info, error) { return mount.Lookup(path) } - -// Unmount unmounts the target. It does not return an error in case the target is not mounted. -// In case the target does not exist, the appropriate error is returned. -func Unmount(target string) error { - err := unix.Unmount(target, unix.MNT_DETACH) - if err == unix.EINVAL { - // ignore "not mounted" error - err = nil - } - - return err -} diff --git a/pkg/os/mount_other.go b/pkg/os/mount_other.go new file mode 100644 index 000000000..3a778d058 --- /dev/null +++ b/pkg/os/mount_other.go @@ -0,0 +1,26 @@ +// +build !windows,!linux + +/* + Copyright The containerd 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 + + http://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. +*/ + +package os + +import "github.com/containerd/containerd/mount" + +// LookupMount gets mount info of a given path. +func (RealOS) LookupMount(path string) (mount.Info, error) { + return mount.Lookup(path) +} diff --git a/pkg/os/mount_unix.go b/pkg/os/mount_unix.go new file mode 100644 index 000000000..e81def359 --- /dev/null +++ b/pkg/os/mount_unix.go @@ -0,0 +1,33 @@ +// +build !windows,!linux + +/* + Copyright The containerd 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 + + http://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. +*/ + +package os + +import ( + "github.com/containerd/containerd/mount" +) + +// Mount will call unix.Mount to mount the file. +func (RealOS) Mount(source string, target string, fstype string, flags uintptr, data string) error { + return mount.ErrNotImplementOnUnix +} + +// Unmount will call Unmount to unmount the file. +func (RealOS) Unmount(target string) error { + return mount.Unmount(target, 0) +} diff --git a/vendor/github.com/containerd/cri/pkg/os/os.go b/pkg/os/os.go similarity index 100% rename from vendor/github.com/containerd/cri/pkg/os/os.go rename to pkg/os/os.go diff --git a/pkg/os/os_unix.go b/pkg/os/os_unix.go new file mode 100644 index 000000000..eaf0984dd --- /dev/null +++ b/pkg/os/os_unix.go @@ -0,0 +1,31 @@ +// +build !windows + +/* + Copyright The containerd 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 + + http://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. +*/ + +package os + +import ( + "github.com/containerd/containerd/mount" +) + +// UNIX collects unix system level operations that need to be +// mocked out during tests. +type UNIX interface { + Mount(source string, target string, fstype string, flags uintptr, data string) error + Unmount(target string) error + LookupMount(path string) (mount.Info, error) +} diff --git a/pkg/os/testing/fake_os.go b/pkg/os/testing/fake_os.go new file mode 100644 index 000000000..c4f1bf6cc --- /dev/null +++ b/pkg/os/testing/fake_os.go @@ -0,0 +1,254 @@ +/* + Copyright The containerd 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 + + http://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. +*/ + +package testing + +import ( + "os" + "sync" + + containerdmount "github.com/containerd/containerd/mount" + + osInterface "github.com/containerd/containerd/pkg/os" +) + +// CalledDetail is the struct contains called function name and arguments. +type CalledDetail struct { + // Name of the function called. + Name string + // Arguments of the function called. + Arguments []interface{} +} + +// FakeOS mocks out certain OS calls to avoid perturbing the filesystem +// If a member of the form `*Fn` is set, that function will be called in place +// of the real call. +type FakeOS struct { + sync.Mutex + MkdirAllFn func(string, os.FileMode) error + RemoveAllFn func(string) error + StatFn func(string) (os.FileInfo, error) + ResolveSymbolicLinkFn func(string) (string, error) + FollowSymlinkInScopeFn func(string, string) (string, error) + CopyFileFn func(string, string, os.FileMode) error + WriteFileFn func(string, []byte, os.FileMode) error + MountFn func(source string, target string, fstype string, flags uintptr, data string) error + UnmountFn func(target string) error + LookupMountFn func(path string) (containerdmount.Info, error) + HostnameFn func() (string, error) + calls []CalledDetail + errors map[string]error +} + +var _ osInterface.OS = &FakeOS{} + +// getError get error for call +func (f *FakeOS) getError(op string) error { + f.Lock() + defer f.Unlock() + err, ok := f.errors[op] + if ok { + delete(f.errors, op) + return err + } + return nil +} + +// InjectError inject error for call +func (f *FakeOS) InjectError(fn string, err error) { + f.Lock() + defer f.Unlock() + f.errors[fn] = err +} + +// InjectErrors inject errors for calls +func (f *FakeOS) InjectErrors(errs map[string]error) { + f.Lock() + defer f.Unlock() + for fn, err := range errs { + f.errors[fn] = err + } +} + +// ClearErrors clear errors for call +func (f *FakeOS) ClearErrors() { + f.Lock() + defer f.Unlock() + f.errors = make(map[string]error) +} + +func (f *FakeOS) appendCalls(name string, args ...interface{}) { + f.Lock() + defer f.Unlock() + f.calls = append(f.calls, CalledDetail{Name: name, Arguments: args}) +} + +// GetCalls get detail of calls. +func (f *FakeOS) GetCalls() []CalledDetail { + f.Lock() + defer f.Unlock() + return append([]CalledDetail{}, f.calls...) +} + +// NewFakeOS creates a FakeOS. +func NewFakeOS() *FakeOS { + return &FakeOS{ + errors: make(map[string]error), + } +} + +// MkdirAll is a fake call that invokes MkdirAllFn or just returns nil. +func (f *FakeOS) MkdirAll(path string, perm os.FileMode) error { + f.appendCalls("MkdirAll", path, perm) + if err := f.getError("MkdirAll"); err != nil { + return err + } + + if f.MkdirAllFn != nil { + return f.MkdirAllFn(path, perm) + } + return nil +} + +// RemoveAll is a fake call that invokes RemoveAllFn or just returns nil. +func (f *FakeOS) RemoveAll(path string) error { + f.appendCalls("RemoveAll", path) + if err := f.getError("RemoveAll"); err != nil { + return err + } + + if f.RemoveAllFn != nil { + return f.RemoveAllFn(path) + } + return nil +} + +// Stat is a fake call that invokes StatFn or just return nil. +func (f *FakeOS) Stat(name string) (os.FileInfo, error) { + f.appendCalls("Stat", name) + if err := f.getError("Stat"); err != nil { + return nil, err + } + + if f.StatFn != nil { + return f.StatFn(name) + } + return nil, nil +} + +// ResolveSymbolicLink is a fake call that invokes ResolveSymbolicLinkFn or returns its input +func (f *FakeOS) ResolveSymbolicLink(path string) (string, error) { + f.appendCalls("ResolveSymbolicLink", path) + if err := f.getError("ResolveSymbolicLink"); err != nil { + return "", err + } + + if f.ResolveSymbolicLinkFn != nil { + return f.ResolveSymbolicLinkFn(path) + } + return path, nil +} + +// FollowSymlinkInScope is a fake call that invokes FollowSymlinkInScope or returns its input +func (f *FakeOS) FollowSymlinkInScope(path, scope string) (string, error) { + f.appendCalls("FollowSymlinkInScope", path, scope) + if err := f.getError("FollowSymlinkInScope"); err != nil { + return "", err + } + + if f.FollowSymlinkInScopeFn != nil { + return f.FollowSymlinkInScopeFn(path, scope) + } + return path, nil +} + +// CopyFile is a fake call that invokes CopyFileFn or just return nil. +func (f *FakeOS) CopyFile(src, dest string, perm os.FileMode) error { + f.appendCalls("CopyFile", src, dest, perm) + if err := f.getError("CopyFile"); err != nil { + return err + } + + if f.CopyFileFn != nil { + return f.CopyFileFn(src, dest, perm) + } + return nil +} + +// WriteFile is a fake call that invokes WriteFileFn or just return nil. +func (f *FakeOS) WriteFile(filename string, data []byte, perm os.FileMode) error { + f.appendCalls("WriteFile", filename, data, perm) + if err := f.getError("WriteFile"); err != nil { + return err + } + + if f.WriteFileFn != nil { + return f.WriteFileFn(filename, data, perm) + } + return nil +} + +// Mount is a fake call that invokes MountFn or just return nil. +func (f *FakeOS) Mount(source string, target string, fstype string, flags uintptr, data string) error { + f.appendCalls("Mount", source, target, fstype, flags, data) + if err := f.getError("Mount"); err != nil { + return err + } + + if f.MountFn != nil { + return f.MountFn(source, target, fstype, flags, data) + } + return nil +} + +// Unmount is a fake call that invokes UnmountFn or just return nil. +func (f *FakeOS) Unmount(target string) error { + f.appendCalls("Unmount", target) + if err := f.getError("Unmount"); err != nil { + return err + } + + if f.UnmountFn != nil { + return f.UnmountFn(target) + } + return nil +} + +// LookupMount is a fake call that invokes LookupMountFn or just return nil. +func (f *FakeOS) LookupMount(path string) (containerdmount.Info, error) { + f.appendCalls("LookupMount", path) + if err := f.getError("LookupMount"); err != nil { + return containerdmount.Info{}, err + } + + if f.LookupMountFn != nil { + return f.LookupMountFn(path) + } + return containerdmount.Info{}, nil +} + +// Hostname is a fake call that invokes HostnameFn or just return nil. +func (f *FakeOS) Hostname() (string, error) { + f.appendCalls("Hostname") + if err := f.getError("Hostname"); err != nil { + return "", err + } + + if f.HostnameFn != nil { + return f.HostnameFn() + } + return "", nil +} diff --git a/pkg/os/testing/fake_os_unix.go b/pkg/os/testing/fake_os_unix.go new file mode 100644 index 000000000..9a1639489 --- /dev/null +++ b/pkg/os/testing/fake_os_unix.go @@ -0,0 +1,23 @@ +// +build !windows + +/* + Copyright The containerd 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 + + http://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. +*/ + +package testing + +import osInterface "github.com/containerd/containerd/pkg/os" + +var _ osInterface.UNIX = &FakeOS{} diff --git a/vendor/github.com/containerd/cri/pkg/registrar/registrar.go b/pkg/registrar/registrar.go similarity index 100% rename from vendor/github.com/containerd/cri/pkg/registrar/registrar.go rename to pkg/registrar/registrar.go diff --git a/pkg/registrar/registrar_test.go b/pkg/registrar/registrar_test.go new file mode 100644 index 000000000..318bfa2fd --- /dev/null +++ b/pkg/registrar/registrar_test.go @@ -0,0 +1,54 @@ +/* + Copyright The containerd 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 + + http://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. +*/ + +package registrar + +import ( + "testing" + + assertlib "github.com/stretchr/testify/assert" +) + +func TestRegistrar(t *testing.T) { + r := NewRegistrar() + assert := assertlib.New(t) + + t.Logf("should be able to reserve a name<->key mapping") + assert.NoError(r.Reserve("test-name-1", "test-id-1")) + + t.Logf("should be able to reserve a new name<->key mapping") + assert.NoError(r.Reserve("test-name-2", "test-id-2")) + + t.Logf("should be able to reserve the same name<->key mapping") + assert.NoError(r.Reserve("test-name-1", "test-id-1")) + + t.Logf("should not be able to reserve conflict name<->key mapping") + assert.Error(r.Reserve("test-name-1", "test-id-conflict")) + assert.Error(r.Reserve("test-name-conflict", "test-id-2")) + + t.Logf("should be able to release name<->key mapping by key") + r.ReleaseByKey("test-id-1") + + t.Logf("should be able to release name<->key mapping by name") + r.ReleaseByName("test-name-2") + + t.Logf("should be able to reserve new name<->key mapping after release") + assert.NoError(r.Reserve("test-name-1", "test-id-new")) + assert.NoError(r.Reserve("test-name-new", "test-id-2")) + + t.Logf("should be able to reserve same name/key name<->key") + assert.NoError(r.Reserve("same-name-id", "same-name-id")) +} diff --git a/pkg/seccomp/fixtures/proc_self_status b/pkg/seccomp/fixtures/proc_self_status new file mode 100644 index 000000000..0e0084f6c --- /dev/null +++ b/pkg/seccomp/fixtures/proc_self_status @@ -0,0 +1,47 @@ +Name: cat +State: R (running) +Tgid: 19383 +Ngid: 0 +Pid: 19383 +PPid: 19275 +TracerPid: 0 +Uid: 1000 1000 1000 1000 +Gid: 1000 1000 1000 1000 +FDSize: 256 +Groups: 24 25 27 29 30 44 46 102 104 108 111 1000 1001 +NStgid: 19383 +NSpid: 19383 +NSpgid: 19383 +NSsid: 19275 +VmPeak: 5944 kB +VmSize: 5944 kB +VmLck: 0 kB +VmPin: 0 kB +VmHWM: 744 kB +VmRSS: 744 kB +VmData: 324 kB +VmStk: 136 kB +VmExe: 48 kB +VmLib: 1776 kB +VmPTE: 32 kB +VmPMD: 12 kB +VmSwap: 0 kB +Threads: 1 +SigQ: 0/30067 +SigPnd: 0000000000000000 +ShdPnd: 0000000000000000 +SigBlk: 0000000000000000 +SigIgn: 0000000000000080 +SigCgt: 0000000000000000 +CapInh: 0000000000000000 +CapPrm: 0000000000000000 +CapEff: 0000000000000000 +CapBnd: 0000003fffffffff +CapAmb: 0000000000000000 +Seccomp: 0 +Cpus_allowed: f +Cpus_allowed_list: 0-3 +Mems_allowed: 00000000,00000001 +Mems_allowed_list: 0 +voluntary_ctxt_switches: 0 +nonvoluntary_ctxt_switches: 1 diff --git a/vendor/github.com/containerd/cri/pkg/seccomp/seccomp_linux.go b/pkg/seccomp/seccomp_linux.go similarity index 100% rename from vendor/github.com/containerd/cri/pkg/seccomp/seccomp_linux.go rename to pkg/seccomp/seccomp_linux.go diff --git a/pkg/seccomp/seccomp_linux_test.go b/pkg/seccomp/seccomp_linux_test.go new file mode 100644 index 000000000..850ab97e1 --- /dev/null +++ b/pkg/seccomp/seccomp_linux_test.go @@ -0,0 +1,48 @@ +/* + Copyright The containerd 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 + + http://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. +*/ + +/* + Copyright The runc 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 + + http://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. +*/ + +package seccomp + +import "testing" + +// TestParseStatusFile is from https://github.com/opencontainers/runc/blob/v1.0.0-rc91/libcontainer/seccomp/seccomp_linux_test.go +func TestParseStatusFile(t *testing.T) { + s, err := parseStatusFile("fixtures/proc_self_status") + if err != nil { + t.Fatal(err) + } + + if _, ok := s["Seccomp"]; !ok { + + t.Fatal("expected to find 'Seccomp' in the map but did not.") + } +} diff --git a/vendor/github.com/containerd/cri/pkg/seccomp/seccomp_unsupported.go b/pkg/seccomp/seccomp_unsupported.go similarity index 100% rename from vendor/github.com/containerd/cri/pkg/seccomp/seccomp_unsupported.go rename to pkg/seccomp/seccomp_unsupported.go diff --git a/vendor/github.com/containerd/cri/pkg/server/bandwidth/doc.go b/pkg/server/bandwidth/doc.go similarity index 100% rename from vendor/github.com/containerd/cri/pkg/server/bandwidth/doc.go rename to pkg/server/bandwidth/doc.go diff --git a/vendor/github.com/containerd/cri/pkg/server/bandwidth/fake_shaper.go b/pkg/server/bandwidth/fake_shaper.go similarity index 100% rename from vendor/github.com/containerd/cri/pkg/server/bandwidth/fake_shaper.go rename to pkg/server/bandwidth/fake_shaper.go diff --git a/vendor/github.com/containerd/cri/pkg/server/bandwidth/interfaces.go b/pkg/server/bandwidth/interfaces.go similarity index 100% rename from vendor/github.com/containerd/cri/pkg/server/bandwidth/interfaces.go rename to pkg/server/bandwidth/interfaces.go diff --git a/vendor/github.com/containerd/cri/pkg/server/bandwidth/linux.go b/pkg/server/bandwidth/linux.go similarity index 100% rename from vendor/github.com/containerd/cri/pkg/server/bandwidth/linux.go rename to pkg/server/bandwidth/linux.go diff --git a/vendor/github.com/containerd/cri/pkg/server/bandwidth/unsupported.go b/pkg/server/bandwidth/unsupported.go similarity index 100% rename from vendor/github.com/containerd/cri/pkg/server/bandwidth/unsupported.go rename to pkg/server/bandwidth/unsupported.go diff --git a/vendor/github.com/containerd/cri/pkg/server/bandwidth/utils.go b/pkg/server/bandwidth/utils.go similarity index 100% rename from vendor/github.com/containerd/cri/pkg/server/bandwidth/utils.go rename to pkg/server/bandwidth/utils.go diff --git a/vendor/github.com/containerd/cri/pkg/server/cni_conf_syncer.go b/pkg/server/cni_conf_syncer.go similarity index 100% rename from vendor/github.com/containerd/cri/pkg/server/cni_conf_syncer.go rename to pkg/server/cni_conf_syncer.go diff --git a/vendor/github.com/containerd/cri/pkg/server/container_attach.go b/pkg/server/container_attach.go similarity index 98% rename from vendor/github.com/containerd/cri/pkg/server/container_attach.go rename to pkg/server/container_attach.go index c8101ff7c..9f06e25ad 100644 --- a/vendor/github.com/containerd/cri/pkg/server/container_attach.go +++ b/pkg/server/container_attach.go @@ -26,7 +26,7 @@ import ( "k8s.io/client-go/tools/remotecommand" runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" - cio "github.com/containerd/cri/pkg/server/io" + cio "github.com/containerd/containerd/pkg/server/io" ) // Attach prepares a streaming endpoint to attach to a running container, and returns the address. diff --git a/vendor/github.com/containerd/cri/pkg/server/container_create.go b/pkg/server/container_create.go similarity index 96% rename from vendor/github.com/containerd/cri/pkg/server/container_create.go rename to pkg/server/container_create.go index 01eac7bc8..ae7762a23 100644 --- a/vendor/github.com/containerd/cri/pkg/server/container_create.go +++ b/pkg/server/container_create.go @@ -33,16 +33,16 @@ import ( "golang.org/x/net/context" runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" - customopts "github.com/containerd/cri/pkg/containerd/opts" - ctrdutil "github.com/containerd/cri/pkg/containerd/util" - cio "github.com/containerd/cri/pkg/server/io" - containerstore "github.com/containerd/cri/pkg/store/container" - "github.com/containerd/cri/pkg/util" + customopts "github.com/containerd/containerd/pkg/containerd/opts" + ctrdutil "github.com/containerd/containerd/pkg/containerd/util" + cio "github.com/containerd/containerd/pkg/server/io" + containerstore "github.com/containerd/containerd/pkg/store/container" + "github.com/containerd/containerd/pkg/util" ) func init() { typeurl.Register(&containerstore.Metadata{}, - "github.com/containerd/cri/pkg/store/container", "Metadata") + "github.com/containerd/containerd/pkg/store/container", "Metadata") } // CreateContainer creates a new container in the given PodSandbox. diff --git a/vendor/github.com/containerd/cri/pkg/server/container_create_unix.go b/pkg/server/container_create_linux.go similarity index 97% rename from vendor/github.com/containerd/cri/pkg/server/container_create_unix.go rename to pkg/server/container_create_linux.go index 28863cb0c..db32b7559 100644 --- a/vendor/github.com/containerd/cri/pkg/server/container_create_unix.go +++ b/pkg/server/container_create_linux.go @@ -1,5 +1,3 @@ -// +build !windows - /* Copyright The containerd Authors. @@ -36,9 +34,9 @@ import ( "github.com/pkg/errors" runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" - "github.com/containerd/cri/pkg/annotations" - "github.com/containerd/cri/pkg/config" - customopts "github.com/containerd/cri/pkg/containerd/opts" + "github.com/containerd/containerd/pkg/annotations" + "github.com/containerd/containerd/pkg/config" + customopts "github.com/containerd/containerd/pkg/containerd/opts" ) const ( @@ -182,11 +180,15 @@ func (c *criService) containerSpec(id string, sandboxID string, sandboxPid uint3 if !c.config.DisableProcMount { // Apply masked paths if specified. // If the container is privileged, this will be cleared later on. - specOpts = append(specOpts, oci.WithMaskedPaths(securityContext.GetMaskedPaths())) + if maskedPaths := securityContext.GetMaskedPaths(); maskedPaths != nil { + specOpts = append(specOpts, oci.WithMaskedPaths(maskedPaths)) + } // Apply readonly paths if specified. // If the container is privileged, this will be cleared later on. - specOpts = append(specOpts, oci.WithReadonlyPaths(securityContext.GetReadonlyPaths())) + if readonlyPaths := securityContext.GetReadonlyPaths(); readonlyPaths != nil { + specOpts = append(specOpts, oci.WithReadonlyPaths(readonlyPaths)) + } } if securityContext.GetPrivileged() { diff --git a/pkg/server/container_create_linux_test.go b/pkg/server/container_create_linux_test.go new file mode 100644 index 000000000..843f8ed08 --- /dev/null +++ b/pkg/server/container_create_linux_test.go @@ -0,0 +1,1253 @@ +/* + Copyright The containerd 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 + + http://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. +*/ + +package server + +import ( + "context" + "fmt" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + + "github.com/containerd/containerd/containers" + "github.com/containerd/containerd/contrib/apparmor" + "github.com/containerd/containerd/contrib/seccomp" + "github.com/containerd/containerd/mount" + "github.com/containerd/containerd/oci" + imagespec "github.com/opencontainers/image-spec/specs-go/v1" + libcontainerconfigs "github.com/opencontainers/runc/libcontainer/configs" + "github.com/opencontainers/runc/libcontainer/devices" + runtimespec "github.com/opencontainers/runtime-spec/specs-go" + "github.com/opencontainers/selinux/go-selinux" + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" + + "github.com/containerd/containerd/pkg/annotations" + "github.com/containerd/containerd/pkg/config" + "github.com/containerd/containerd/pkg/containerd/opts" + ctrdutil "github.com/containerd/containerd/pkg/containerd/util" + ostesting "github.com/containerd/containerd/pkg/os/testing" + "github.com/containerd/containerd/pkg/util" +) + +func getCreateContainerTestData() (*runtime.ContainerConfig, *runtime.PodSandboxConfig, + *imagespec.ImageConfig, func(*testing.T, string, string, uint32, *runtimespec.Spec)) { + config := &runtime.ContainerConfig{ + Metadata: &runtime.ContainerMetadata{ + Name: "test-name", + Attempt: 1, + }, + Image: &runtime.ImageSpec{ + Image: "sha256:c75bebcdd211f41b3a460c7bf82970ed6c75acaab9cd4c9a4e125b03ca113799", + }, + Command: []string{"test", "command"}, + Args: []string{"test", "args"}, + WorkingDir: "test-cwd", + Envs: []*runtime.KeyValue{ + {Key: "k1", Value: "v1"}, + {Key: "k2", Value: "v2"}, + {Key: "k3", Value: "v3=v3bis"}, + {Key: "k4", Value: "v4=v4bis=foop"}, + }, + Mounts: []*runtime.Mount{ + // everything default + { + ContainerPath: "container-path-1", + HostPath: "host-path-1", + }, + // readOnly + { + ContainerPath: "container-path-2", + HostPath: "host-path-2", + Readonly: true, + }, + }, + Labels: map[string]string{"a": "b"}, + Annotations: map[string]string{"ca-c": "ca-d"}, + Linux: &runtime.LinuxContainerConfig{ + Resources: &runtime.LinuxContainerResources{ + CpuPeriod: 100, + CpuQuota: 200, + CpuShares: 300, + MemoryLimitInBytes: 400, + OomScoreAdj: 500, + CpusetCpus: "0-1", + CpusetMems: "2-3", + }, + SecurityContext: &runtime.LinuxContainerSecurityContext{ + SupplementalGroups: []int64{1111, 2222}, + NoNewPrivs: true, + }, + }, + } + sandboxConfig := &runtime.PodSandboxConfig{ + Metadata: &runtime.PodSandboxMetadata{ + Name: "test-sandbox-name", + Uid: "test-sandbox-uid", + Namespace: "test-sandbox-ns", + Attempt: 2, + }, + Annotations: map[string]string{"c": "d"}, + Linux: &runtime.LinuxPodSandboxConfig{ + CgroupParent: "/test/cgroup/parent", + SecurityContext: &runtime.LinuxSandboxSecurityContext{}, + }, + } + imageConfig := &imagespec.ImageConfig{ + Env: []string{"ik1=iv1", "ik2=iv2", "ik3=iv3=iv3bis", "ik4=iv4=iv4bis=boop"}, + Entrypoint: []string{"/entrypoint"}, + Cmd: []string{"cmd"}, + WorkingDir: "/workspace", + } + specCheck := func(t *testing.T, id string, sandboxID string, sandboxPid uint32, spec *runtimespec.Spec) { + assert.Equal(t, relativeRootfsPath, spec.Root.Path) + assert.Equal(t, []string{"test", "command", "test", "args"}, spec.Process.Args) + assert.Equal(t, "test-cwd", spec.Process.Cwd) + assert.Contains(t, spec.Process.Env, "k1=v1", "k2=v2", "k3=v3=v3bis", "ik4=iv4=iv4bis=boop") + assert.Contains(t, spec.Process.Env, "ik1=iv1", "ik2=iv2", "ik3=iv3=iv3bis", "k4=v4=v4bis=foop") + + t.Logf("Check cgroups bind mount") + checkMount(t, spec.Mounts, "cgroup", "/sys/fs/cgroup", "cgroup", []string{"ro"}, nil) + + t.Logf("Check bind mount") + checkMount(t, spec.Mounts, "host-path-1", "container-path-1", "bind", []string{"rbind", "rprivate", "rw"}, nil) + checkMount(t, spec.Mounts, "host-path-2", "container-path-2", "bind", []string{"rbind", "rprivate", "ro"}, nil) + + t.Logf("Check resource limits") + assert.EqualValues(t, *spec.Linux.Resources.CPU.Period, 100) + assert.EqualValues(t, *spec.Linux.Resources.CPU.Quota, 200) + assert.EqualValues(t, *spec.Linux.Resources.CPU.Shares, 300) + assert.EqualValues(t, spec.Linux.Resources.CPU.Cpus, "0-1") + assert.EqualValues(t, spec.Linux.Resources.CPU.Mems, "2-3") + assert.EqualValues(t, *spec.Linux.Resources.Memory.Limit, 400) + assert.EqualValues(t, *spec.Process.OOMScoreAdj, 500) + + t.Logf("Check supplemental groups") + assert.Contains(t, spec.Process.User.AdditionalGids, uint32(1111)) + assert.Contains(t, spec.Process.User.AdditionalGids, uint32(2222)) + + t.Logf("Check no_new_privs") + assert.Equal(t, spec.Process.NoNewPrivileges, true) + + t.Logf("Check cgroup path") + assert.Equal(t, getCgroupsPath("/test/cgroup/parent", id), spec.Linux.CgroupsPath) + + t.Logf("Check namespaces") + assert.Contains(t, spec.Linux.Namespaces, runtimespec.LinuxNamespace{ + Type: runtimespec.NetworkNamespace, + Path: opts.GetNetworkNamespace(sandboxPid), + }) + assert.Contains(t, spec.Linux.Namespaces, runtimespec.LinuxNamespace{ + Type: runtimespec.IPCNamespace, + Path: opts.GetIPCNamespace(sandboxPid), + }) + assert.Contains(t, spec.Linux.Namespaces, runtimespec.LinuxNamespace{ + Type: runtimespec.UTSNamespace, + Path: opts.GetUTSNamespace(sandboxPid), + }) + assert.Contains(t, spec.Linux.Namespaces, runtimespec.LinuxNamespace{ + Type: runtimespec.PIDNamespace, + Path: opts.GetPIDNamespace(sandboxPid), + }) + + t.Logf("Check PodSandbox annotations") + assert.Contains(t, spec.Annotations, annotations.SandboxID) + assert.EqualValues(t, spec.Annotations[annotations.SandboxID], sandboxID) + + assert.Contains(t, spec.Annotations, annotations.ContainerType) + assert.EqualValues(t, spec.Annotations[annotations.ContainerType], annotations.ContainerTypeContainer) + } + return config, sandboxConfig, imageConfig, specCheck +} + +func TestContainerCapabilities(t *testing.T) { + testID := "test-id" + testSandboxID := "sandbox-id" + testContainerName := "container-name" + testPid := uint32(1234) + for desc, test := range map[string]struct { + capability *runtime.Capability + includes []string + excludes []string + }{ + "should be able to add/drop capabilities": { + capability: &runtime.Capability{ + AddCapabilities: []string{"SYS_ADMIN"}, + DropCapabilities: []string{"CHOWN"}, + }, + includes: []string{"CAP_SYS_ADMIN"}, + excludes: []string{"CAP_CHOWN"}, + }, + "should be able to add all capabilities": { + capability: &runtime.Capability{ + AddCapabilities: []string{"ALL"}, + }, + includes: oci.GetAllCapabilities(), + }, + "should be able to drop all capabilities": { + capability: &runtime.Capability{ + DropCapabilities: []string{"ALL"}, + }, + excludes: oci.GetAllCapabilities(), + }, + "should be able to drop capabilities with add all": { + capability: &runtime.Capability{ + AddCapabilities: []string{"ALL"}, + DropCapabilities: []string{"CHOWN"}, + }, + includes: util.SubtractStringSlice(oci.GetAllCapabilities(), "CAP_CHOWN"), + excludes: []string{"CAP_CHOWN"}, + }, + "should be able to add capabilities with drop all": { + capability: &runtime.Capability{ + AddCapabilities: []string{"SYS_ADMIN"}, + DropCapabilities: []string{"ALL"}, + }, + includes: []string{"CAP_SYS_ADMIN"}, + excludes: util.SubtractStringSlice(oci.GetAllCapabilities(), "CAP_SYS_ADMIN"), + }, + } { + t.Logf("TestCase %q", desc) + containerConfig, sandboxConfig, imageConfig, specCheck := getCreateContainerTestData() + ociRuntime := config.Runtime{} + c := newTestCRIService() + + containerConfig.Linux.SecurityContext.Capabilities = test.capability + spec, err := c.containerSpec(testID, testSandboxID, testPid, "", testContainerName, containerConfig, sandboxConfig, imageConfig, nil, ociRuntime) + require.NoError(t, err) + + if selinux.GetEnabled() { + assert.NotEqual(t, "", spec.Process.SelinuxLabel) + assert.NotEqual(t, "", spec.Linux.MountLabel) + } + + specCheck(t, testID, testSandboxID, testPid, spec) + for _, include := range test.includes { + assert.Contains(t, spec.Process.Capabilities.Bounding, include) + assert.Contains(t, spec.Process.Capabilities.Effective, include) + assert.Contains(t, spec.Process.Capabilities.Inheritable, include) + assert.Contains(t, spec.Process.Capabilities.Permitted, include) + } + for _, exclude := range test.excludes { + assert.NotContains(t, spec.Process.Capabilities.Bounding, exclude) + assert.NotContains(t, spec.Process.Capabilities.Effective, exclude) + assert.NotContains(t, spec.Process.Capabilities.Inheritable, exclude) + assert.NotContains(t, spec.Process.Capabilities.Permitted, exclude) + } + assert.Empty(t, spec.Process.Capabilities.Ambient) + } +} + +func TestContainerSpecTty(t *testing.T) { + testID := "test-id" + testSandboxID := "sandbox-id" + testContainerName := "container-name" + testPid := uint32(1234) + containerConfig, sandboxConfig, imageConfig, specCheck := getCreateContainerTestData() + ociRuntime := config.Runtime{} + c := newTestCRIService() + for _, tty := range []bool{true, false} { + containerConfig.Tty = tty + spec, err := c.containerSpec(testID, testSandboxID, testPid, "", testContainerName, containerConfig, sandboxConfig, imageConfig, nil, ociRuntime) + require.NoError(t, err) + specCheck(t, testID, testSandboxID, testPid, spec) + assert.Equal(t, tty, spec.Process.Terminal) + if tty { + assert.Contains(t, spec.Process.Env, "TERM=xterm") + } else { + assert.NotContains(t, spec.Process.Env, "TERM=xterm") + } + } +} + +func TestContainerSpecDefaultPath(t *testing.T) { + testID := "test-id" + testSandboxID := "sandbox-id" + testContainerName := "container-name" + testPid := uint32(1234) + expectedDefault := "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + containerConfig, sandboxConfig, imageConfig, specCheck := getCreateContainerTestData() + ociRuntime := config.Runtime{} + c := newTestCRIService() + for _, pathenv := range []string{"", "PATH=/usr/local/bin/games"} { + expected := expectedDefault + if pathenv != "" { + imageConfig.Env = append(imageConfig.Env, pathenv) + expected = pathenv + } + spec, err := c.containerSpec(testID, testSandboxID, testPid, "", testContainerName, containerConfig, sandboxConfig, imageConfig, nil, ociRuntime) + require.NoError(t, err) + specCheck(t, testID, testSandboxID, testPid, spec) + assert.Contains(t, spec.Process.Env, expected) + } +} + +func TestContainerSpecReadonlyRootfs(t *testing.T) { + testID := "test-id" + testSandboxID := "sandbox-id" + testContainerName := "container-name" + testPid := uint32(1234) + containerConfig, sandboxConfig, imageConfig, specCheck := getCreateContainerTestData() + ociRuntime := config.Runtime{} + c := newTestCRIService() + for _, readonly := range []bool{true, false} { + containerConfig.Linux.SecurityContext.ReadonlyRootfs = readonly + spec, err := c.containerSpec(testID, testSandboxID, testPid, "", testContainerName, containerConfig, sandboxConfig, imageConfig, nil, ociRuntime) + require.NoError(t, err) + specCheck(t, testID, testSandboxID, testPid, spec) + assert.Equal(t, readonly, spec.Root.Readonly) + } +} + +func TestContainerSpecWithExtraMounts(t *testing.T) { + testID := "test-id" + testSandboxID := "sandbox-id" + testContainerName := "container-name" + testPid := uint32(1234) + containerConfig, sandboxConfig, imageConfig, specCheck := getCreateContainerTestData() + ociRuntime := config.Runtime{} + c := newTestCRIService() + mountInConfig := &runtime.Mount{ + // Test cleanpath + ContainerPath: "test-container-path/", + HostPath: "test-host-path", + Readonly: false, + } + containerConfig.Mounts = append(containerConfig.Mounts, mountInConfig) + extraMounts := []*runtime.Mount{ + { + ContainerPath: "test-container-path", + HostPath: "test-host-path-extra", + Readonly: true, + }, + { + ContainerPath: "/sys", + HostPath: "test-sys-extra", + Readonly: false, + }, + { + ContainerPath: "/dev", + HostPath: "test-dev-extra", + Readonly: false, + }, + } + spec, err := c.containerSpec(testID, testSandboxID, testPid, "", testContainerName, containerConfig, sandboxConfig, imageConfig, extraMounts, ociRuntime) + require.NoError(t, err) + specCheck(t, testID, testSandboxID, testPid, spec) + var mounts, sysMounts, devMounts []runtimespec.Mount + for _, m := range spec.Mounts { + if strings.HasPrefix(m.Destination, "test-container-path") { + mounts = append(mounts, m) + } else if m.Destination == "/sys" { + sysMounts = append(sysMounts, m) + } else if strings.HasPrefix(m.Destination, "/dev") { + devMounts = append(devMounts, m) + } + } + t.Logf("CRI mount should override extra mount") + require.Len(t, mounts, 1) + assert.Equal(t, "test-host-path", mounts[0].Source) + assert.Contains(t, mounts[0].Options, "rw") + + t.Logf("Extra mount should override default mount") + require.Len(t, sysMounts, 1) + assert.Equal(t, "test-sys-extra", sysMounts[0].Source) + assert.Contains(t, sysMounts[0].Options, "rw") + + t.Logf("Dev mount should override all default dev mounts") + require.Len(t, devMounts, 1) + assert.Equal(t, "test-dev-extra", devMounts[0].Source) + assert.Contains(t, devMounts[0].Options, "rw") +} + +func TestContainerAndSandboxPrivileged(t *testing.T) { + testID := "test-id" + testSandboxID := "sandbox-id" + testContainerName := "container-name" + testPid := uint32(1234) + containerConfig, sandboxConfig, imageConfig, _ := getCreateContainerTestData() + ociRuntime := config.Runtime{} + c := newTestCRIService() + for desc, test := range map[string]struct { + containerPrivileged bool + sandboxPrivileged bool + expectError bool + }{ + "privileged container in non-privileged sandbox should fail": { + containerPrivileged: true, + sandboxPrivileged: false, + expectError: true, + }, + "privileged container in privileged sandbox should be fine": { + containerPrivileged: true, + sandboxPrivileged: true, + expectError: false, + }, + "non-privileged container in privileged sandbox should be fine": { + containerPrivileged: false, + sandboxPrivileged: true, + expectError: false, + }, + "non-privileged container in non-privileged sandbox should be fine": { + containerPrivileged: false, + sandboxPrivileged: false, + expectError: false, + }, + } { + t.Logf("TestCase %q", desc) + containerConfig.Linux.SecurityContext.Privileged = test.containerPrivileged + sandboxConfig.Linux.SecurityContext = &runtime.LinuxSandboxSecurityContext{ + Privileged: test.sandboxPrivileged, + } + _, err := c.containerSpec(testID, testSandboxID, testPid, "", testContainerName, containerConfig, sandboxConfig, imageConfig, nil, ociRuntime) + if test.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + } +} + +func TestContainerMounts(t *testing.T) { + const testSandboxID = "test-id" + for desc, test := range map[string]struct { + statFn func(string) (os.FileInfo, error) + criMounts []*runtime.Mount + securityContext *runtime.LinuxContainerSecurityContext + expectedMounts []*runtime.Mount + }{ + "should setup ro mount when rootfs is read-only": { + securityContext: &runtime.LinuxContainerSecurityContext{ + ReadonlyRootfs: true, + }, + expectedMounts: []*runtime.Mount{ + { + ContainerPath: "/etc/hostname", + HostPath: filepath.Join(testRootDir, sandboxesDir, testSandboxID, "hostname"), + Readonly: true, + }, + { + ContainerPath: "/etc/hosts", + HostPath: filepath.Join(testRootDir, sandboxesDir, testSandboxID, "hosts"), + Readonly: true, + }, + { + ContainerPath: resolvConfPath, + HostPath: filepath.Join(testRootDir, sandboxesDir, testSandboxID, "resolv.conf"), + Readonly: true, + }, + { + ContainerPath: "/dev/shm", + HostPath: filepath.Join(testStateDir, sandboxesDir, testSandboxID, "shm"), + Readonly: false, + }, + }, + }, + "should setup rw mount when rootfs is read-write": { + securityContext: &runtime.LinuxContainerSecurityContext{}, + expectedMounts: []*runtime.Mount{ + { + ContainerPath: "/etc/hostname", + HostPath: filepath.Join(testRootDir, sandboxesDir, testSandboxID, "hostname"), + Readonly: false, + }, + { + ContainerPath: "/etc/hosts", + HostPath: filepath.Join(testRootDir, sandboxesDir, testSandboxID, "hosts"), + Readonly: false, + }, + { + ContainerPath: resolvConfPath, + HostPath: filepath.Join(testRootDir, sandboxesDir, testSandboxID, "resolv.conf"), + Readonly: false, + }, + { + ContainerPath: "/dev/shm", + HostPath: filepath.Join(testStateDir, sandboxesDir, testSandboxID, "shm"), + Readonly: false, + }, + }, + }, + "should use host /dev/shm when host ipc is set": { + securityContext: &runtime.LinuxContainerSecurityContext{ + NamespaceOptions: &runtime.NamespaceOption{Ipc: runtime.NamespaceMode_NODE}, + }, + expectedMounts: []*runtime.Mount{ + { + ContainerPath: "/etc/hostname", + HostPath: filepath.Join(testRootDir, sandboxesDir, testSandboxID, "hostname"), + Readonly: false, + }, + { + ContainerPath: "/etc/hosts", + HostPath: filepath.Join(testRootDir, sandboxesDir, testSandboxID, "hosts"), + Readonly: false, + }, + { + ContainerPath: resolvConfPath, + HostPath: filepath.Join(testRootDir, sandboxesDir, testSandboxID, "resolv.conf"), + Readonly: false, + }, + { + ContainerPath: "/dev/shm", + HostPath: "/dev/shm", + Readonly: false, + }, + }, + }, + "should skip container mounts if already mounted by CRI": { + criMounts: []*runtime.Mount{ + { + ContainerPath: "/etc/hostname", + HostPath: "/test-etc-hostname", + }, + { + ContainerPath: "/etc/hosts", + HostPath: "/test-etc-host", + }, + { + ContainerPath: resolvConfPath, + HostPath: "test-resolv-conf", + }, + { + ContainerPath: "/dev/shm", + HostPath: "test-dev-shm", + }, + }, + securityContext: &runtime.LinuxContainerSecurityContext{}, + expectedMounts: nil, + }, + "should skip hostname mount if the old sandbox doesn't have hostname file": { + statFn: func(path string) (os.FileInfo, error) { + assert.Equal(t, filepath.Join(testRootDir, sandboxesDir, testSandboxID, "hostname"), path) + return nil, errors.New("random error") + }, + securityContext: &runtime.LinuxContainerSecurityContext{}, + expectedMounts: []*runtime.Mount{ + { + ContainerPath: "/etc/hosts", + HostPath: filepath.Join(testRootDir, sandboxesDir, testSandboxID, "hosts"), + Readonly: false, + }, + { + ContainerPath: resolvConfPath, + HostPath: filepath.Join(testRootDir, sandboxesDir, testSandboxID, "resolv.conf"), + Readonly: false, + }, + { + ContainerPath: "/dev/shm", + HostPath: filepath.Join(testStateDir, sandboxesDir, testSandboxID, "shm"), + Readonly: false, + }, + }, + }, + } { + config := &runtime.ContainerConfig{ + Metadata: &runtime.ContainerMetadata{ + Name: "test-name", + Attempt: 1, + }, + Mounts: test.criMounts, + Linux: &runtime.LinuxContainerConfig{ + SecurityContext: test.securityContext, + }, + } + c := newTestCRIService() + c.os.(*ostesting.FakeOS).StatFn = test.statFn + mounts := c.containerMounts(testSandboxID, config) + assert.Equal(t, test.expectedMounts, mounts, desc) + } +} + +func TestPrivilegedBindMount(t *testing.T) { + testPid := uint32(1234) + c := newTestCRIService() + testSandboxID := "sandbox-id" + testContainerName := "container-name" + containerConfig, sandboxConfig, imageConfig, _ := getCreateContainerTestData() + ociRuntime := config.Runtime{} + + for desc, test := range map[string]struct { + privileged bool + expectedSysFSRO bool + expectedCgroupFSRO bool + }{ + "sysfs and cgroupfs should mount as 'ro' by default": { + expectedSysFSRO: true, + expectedCgroupFSRO: true, + }, + "sysfs and cgroupfs should not mount as 'ro' if privileged": { + privileged: true, + expectedSysFSRO: false, + expectedCgroupFSRO: false, + }, + } { + t.Logf("TestCase %q", desc) + + containerConfig.Linux.SecurityContext.Privileged = test.privileged + sandboxConfig.Linux.SecurityContext.Privileged = test.privileged + + spec, err := c.containerSpec(t.Name(), testSandboxID, testPid, "", testContainerName, containerConfig, sandboxConfig, imageConfig, nil, ociRuntime) + + assert.NoError(t, err) + if test.expectedSysFSRO { + checkMount(t, spec.Mounts, "sysfs", "/sys", "sysfs", []string{"ro"}, []string{"rw"}) + } else { + checkMount(t, spec.Mounts, "sysfs", "/sys", "sysfs", []string{"rw"}, []string{"ro"}) + } + if test.expectedCgroupFSRO { + checkMount(t, spec.Mounts, "cgroup", "/sys/fs/cgroup", "cgroup", []string{"ro"}, []string{"rw"}) + } else { + checkMount(t, spec.Mounts, "cgroup", "/sys/fs/cgroup", "cgroup", []string{"rw"}, []string{"ro"}) + } + } +} + +func TestMountPropagation(t *testing.T) { + + sharedLookupMountFn := func(string) (mount.Info, error) { + return mount.Info{ + Mountpoint: "host-path", + Optional: "shared:", + }, nil + } + + slaveLookupMountFn := func(string) (mount.Info, error) { + return mount.Info{ + Mountpoint: "host-path", + Optional: "master:", + }, nil + } + + othersLookupMountFn := func(string) (mount.Info, error) { + return mount.Info{ + Mountpoint: "host-path", + Optional: "others", + }, nil + } + + for desc, test := range map[string]struct { + criMount *runtime.Mount + fakeLookupMountFn func(string) (mount.Info, error) + optionsCheck []string + expectErr bool + }{ + "HostPath should mount as 'rprivate' if propagation is MountPropagation_PROPAGATION_PRIVATE": { + criMount: &runtime.Mount{ + ContainerPath: "container-path", + HostPath: "host-path", + Propagation: runtime.MountPropagation_PROPAGATION_PRIVATE, + }, + fakeLookupMountFn: nil, + optionsCheck: []string{"rbind", "rprivate"}, + expectErr: false, + }, + "HostPath should mount as 'rslave' if propagation is MountPropagation_PROPAGATION_HOST_TO_CONTAINER": { + criMount: &runtime.Mount{ + ContainerPath: "container-path", + HostPath: "host-path", + Propagation: runtime.MountPropagation_PROPAGATION_HOST_TO_CONTAINER, + }, + fakeLookupMountFn: slaveLookupMountFn, + optionsCheck: []string{"rbind", "rslave"}, + expectErr: false, + }, + "HostPath should mount as 'rshared' if propagation is MountPropagation_PROPAGATION_BIDIRECTIONAL": { + criMount: &runtime.Mount{ + ContainerPath: "container-path", + HostPath: "host-path", + Propagation: runtime.MountPropagation_PROPAGATION_BIDIRECTIONAL, + }, + fakeLookupMountFn: sharedLookupMountFn, + optionsCheck: []string{"rbind", "rshared"}, + expectErr: false, + }, + "HostPath should mount as 'rprivate' if propagation is illegal": { + criMount: &runtime.Mount{ + ContainerPath: "container-path", + HostPath: "host-path", + Propagation: runtime.MountPropagation(42), + }, + fakeLookupMountFn: nil, + optionsCheck: []string{"rbind", "rprivate"}, + expectErr: false, + }, + "Expect an error if HostPath isn't shared and mount propagation is MountPropagation_PROPAGATION_BIDIRECTIONAL": { + criMount: &runtime.Mount{ + ContainerPath: "container-path", + HostPath: "host-path", + Propagation: runtime.MountPropagation_PROPAGATION_BIDIRECTIONAL, + }, + fakeLookupMountFn: slaveLookupMountFn, + expectErr: true, + }, + "Expect an error if HostPath isn't slave or shared and mount propagation is MountPropagation_PROPAGATION_HOST_TO_CONTAINER": { + criMount: &runtime.Mount{ + ContainerPath: "container-path", + HostPath: "host-path", + Propagation: runtime.MountPropagation_PROPAGATION_HOST_TO_CONTAINER, + }, + fakeLookupMountFn: othersLookupMountFn, + expectErr: true, + }, + } { + t.Logf("TestCase %q", desc) + c := newTestCRIService() + c.os.(*ostesting.FakeOS).LookupMountFn = test.fakeLookupMountFn + config, _, _, _ := getCreateContainerTestData() + + var spec runtimespec.Spec + spec.Linux = &runtimespec.Linux{} + + err := opts.WithMounts(c.os, config, []*runtime.Mount{test.criMount}, "")(context.Background(), nil, nil, &spec) + if test.expectErr { + require.Error(t, err) + } else { + require.NoError(t, err) + checkMount(t, spec.Mounts, test.criMount.HostPath, test.criMount.ContainerPath, "bind", test.optionsCheck, nil) + } + } +} + +func TestPidNamespace(t *testing.T) { + testID := "test-id" + testPid := uint32(1234) + testSandboxID := "sandbox-id" + testContainerName := "container-name" + containerConfig, sandboxConfig, imageConfig, _ := getCreateContainerTestData() + ociRuntime := config.Runtime{} + c := newTestCRIService() + for desc, test := range map[string]struct { + pidNS runtime.NamespaceMode + expected runtimespec.LinuxNamespace + }{ + "node namespace mode": { + pidNS: runtime.NamespaceMode_NODE, + expected: runtimespec.LinuxNamespace{ + Type: runtimespec.PIDNamespace, + Path: opts.GetPIDNamespace(testPid), + }, + }, + "container namespace mode": { + pidNS: runtime.NamespaceMode_CONTAINER, + expected: runtimespec.LinuxNamespace{ + Type: runtimespec.PIDNamespace, + }, + }, + "pod namespace mode": { + pidNS: runtime.NamespaceMode_POD, + expected: runtimespec.LinuxNamespace{ + Type: runtimespec.PIDNamespace, + Path: opts.GetPIDNamespace(testPid), + }, + }, + } { + t.Logf("TestCase %q", desc) + containerConfig.Linux.SecurityContext.NamespaceOptions = &runtime.NamespaceOption{Pid: test.pidNS} + spec, err := c.containerSpec(testID, testSandboxID, testPid, "", testContainerName, containerConfig, sandboxConfig, imageConfig, nil, ociRuntime) + require.NoError(t, err) + assert.Contains(t, spec.Linux.Namespaces, test.expected) + } +} + +func TestNoDefaultRunMount(t *testing.T) { + testID := "test-id" + testPid := uint32(1234) + testSandboxID := "sandbox-id" + testContainerName := "container-name" + containerConfig, sandboxConfig, imageConfig, _ := getCreateContainerTestData() + ociRuntime := config.Runtime{} + c := newTestCRIService() + + spec, err := c.containerSpec(testID, testSandboxID, testPid, "", testContainerName, containerConfig, sandboxConfig, imageConfig, nil, ociRuntime) + assert.NoError(t, err) + for _, mount := range spec.Mounts { + assert.NotEqual(t, "/run", mount.Destination) + } +} + +func TestGenerateSeccompSpecOpts(t *testing.T) { + for desc, test := range map[string]struct { + profile string + privileged bool + disable bool + specOpts oci.SpecOpts + expectErr bool + defaultProfile string + }{ + "should return error if seccomp is specified when seccomp is not supported": { + profile: runtimeDefault, + disable: true, + expectErr: true, + }, + "should not return error if seccomp is not specified when seccomp is not supported": { + profile: "", + disable: true, + }, + "should not return error if seccomp is unconfined when seccomp is not supported": { + profile: unconfinedProfile, + disable: true, + }, + "should not set seccomp when privileged is true": { + profile: seccompDefaultProfile, + privileged: true, + }, + "should not set seccomp when seccomp is unconfined": { + profile: unconfinedProfile, + }, + "should not set seccomp when seccomp is not specified": { + profile: "", + }, + "should set default seccomp when seccomp is runtime/default": { + profile: runtimeDefault, + specOpts: seccomp.WithDefaultProfile(), + }, + "should set default seccomp when seccomp is docker/default": { + profile: dockerDefault, + specOpts: seccomp.WithDefaultProfile(), + }, + "should set specified profile when local profile is specified": { + profile: profileNamePrefix + "test-profile", + specOpts: seccomp.WithProfile("test-profile"), + }, + "should return error if specified profile is invalid": { + profile: "test-profile", + expectErr: true, + }, + "should use default profile when seccomp is empty": { + defaultProfile: profileNamePrefix + "test-profile", + specOpts: seccomp.WithProfile("test-profile"), + }, + "should fallback to docker/default when seccomp is empty and default is runtime/default": { + defaultProfile: runtimeDefault, + specOpts: seccomp.WithDefaultProfile(), + }, + } { + t.Run(fmt.Sprintf("TestCase %q", desc), func(t *testing.T) { + cri := &criService{} + cri.config.UnsetSeccompProfile = test.defaultProfile + specOpts, err := cri.generateSeccompSpecOpts(test.profile, test.privileged, !test.disable) + assert.Equal(t, + reflect.ValueOf(test.specOpts).Pointer(), + reflect.ValueOf(specOpts).Pointer()) + if test.expectErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestGenerateApparmorSpecOpts(t *testing.T) { + for desc, test := range map[string]struct { + profile string + privileged bool + disable bool + specOpts oci.SpecOpts + expectErr bool + }{ + "should return error if apparmor is specified when apparmor is not supported": { + profile: runtimeDefault, + disable: true, + expectErr: true, + }, + "should not return error if apparmor is not specified when apparmor is not supported": { + profile: "", + disable: true, + }, + "should set default apparmor when apparmor is not specified": { + profile: "", + specOpts: apparmor.WithDefaultProfile(appArmorDefaultProfileName), + }, + "should not apparmor when apparmor is not specified and privileged is true": { + profile: "", + privileged: true, + }, + "should not return error if apparmor is unconfined when apparmor is not supported": { + profile: unconfinedProfile, + disable: true, + }, + "should not apparmor when apparmor is unconfined": { + profile: unconfinedProfile, + }, + "should not apparmor when apparmor is unconfined and privileged is true": { + profile: unconfinedProfile, + privileged: true, + }, + "should set default apparmor when apparmor is runtime/default": { + profile: runtimeDefault, + specOpts: apparmor.WithDefaultProfile(appArmorDefaultProfileName), + }, + "should not apparmor when apparmor is default and privileged is true": { + profile: runtimeDefault, + privileged: true, + }, + // TODO (mikebrow) add success with existing defined profile tests + "should return error when undefined local profile is specified": { + profile: profileNamePrefix + "test-profile", + expectErr: true, + }, + "should return error when undefined local profile is specified and privileged is true": { + profile: profileNamePrefix + "test-profile", + privileged: true, + expectErr: true, + }, + "should return error if specified profile is invalid": { + profile: "test-profile", + expectErr: true, + }, + } { + t.Logf("TestCase %q", desc) + specOpts, err := generateApparmorSpecOpts(test.profile, test.privileged, !test.disable) + assert.Equal(t, + reflect.ValueOf(test.specOpts).Pointer(), + reflect.ValueOf(specOpts).Pointer()) + if test.expectErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + } +} + +func TestMaskedAndReadonlyPaths(t *testing.T) { + testID := "test-id" + testSandboxID := "sandbox-id" + testContainerName := "container-name" + testPid := uint32(1234) + containerConfig, sandboxConfig, imageConfig, specCheck := getCreateContainerTestData() + ociRuntime := config.Runtime{} + c := newTestCRIService() + + defaultSpec, err := oci.GenerateSpec(ctrdutil.NamespacedContext(), nil, &containers.Container{ID: testID}) + require.NoError(t, err) + + for desc, test := range map[string]struct { + disableProcMount bool + masked []string + readonly []string + expectedMasked []string + expectedReadonly []string + privileged bool + }{ + "should apply default if not specified when disable_proc_mount = true": { + disableProcMount: true, + masked: nil, + readonly: nil, + expectedMasked: defaultSpec.Linux.MaskedPaths, + expectedReadonly: defaultSpec.Linux.ReadonlyPaths, + privileged: false, + }, + "should apply default if not specified when disable_proc_mount = false": { + disableProcMount: false, + masked: nil, + readonly: nil, + expectedMasked: defaultSpec.Linux.MaskedPaths, + expectedReadonly: defaultSpec.Linux.ReadonlyPaths, + privileged: false, + }, + "should be able to specify empty paths": { + masked: []string{}, + readonly: []string{}, + expectedMasked: []string{}, + expectedReadonly: []string{}, + privileged: false, + }, + "should apply CRI specified paths": { + masked: []string{"/proc"}, + readonly: []string{"/sys"}, + expectedMasked: []string{"/proc"}, + expectedReadonly: []string{"/sys"}, + privileged: false, + }, + "default should be nil for privileged": { + expectedMasked: nil, + expectedReadonly: nil, + privileged: true, + }, + "should be able to specify empty paths, esp. if privileged": { + masked: []string{}, + readonly: []string{}, + expectedMasked: nil, + expectedReadonly: nil, + privileged: true, + }, + "should not apply CRI specified paths if privileged": { + masked: []string{"/proc"}, + readonly: []string{"/sys"}, + expectedMasked: nil, + expectedReadonly: nil, + privileged: true, + }, + } { + t.Logf("TestCase %q", desc) + c.config.DisableProcMount = test.disableProcMount + containerConfig.Linux.SecurityContext.MaskedPaths = test.masked + containerConfig.Linux.SecurityContext.ReadonlyPaths = test.readonly + containerConfig.Linux.SecurityContext.Privileged = test.privileged + sandboxConfig.Linux.SecurityContext = &runtime.LinuxSandboxSecurityContext{ + Privileged: test.privileged, + } + spec, err := c.containerSpec(testID, testSandboxID, testPid, "", testContainerName, containerConfig, sandboxConfig, imageConfig, nil, ociRuntime) + require.NoError(t, err) + if !test.privileged { // specCheck presumes an unprivileged container + specCheck(t, testID, testSandboxID, testPid, spec) + } + assert.Equal(t, test.expectedMasked, spec.Linux.MaskedPaths) + assert.Equal(t, test.expectedReadonly, spec.Linux.ReadonlyPaths) + } +} + +func TestHostname(t *testing.T) { + testID := "test-id" + testSandboxID := "sandbox-id" + testContainerName := "container-name" + testPid := uint32(1234) + containerConfig, sandboxConfig, imageConfig, specCheck := getCreateContainerTestData() + ociRuntime := config.Runtime{} + c := newTestCRIService() + c.os.(*ostesting.FakeOS).HostnameFn = func() (string, error) { + return "real-hostname", nil + } + for desc, test := range map[string]struct { + hostname string + networkNs runtime.NamespaceMode + expectedEnv string + }{ + "should add HOSTNAME=sandbox.Hostname for pod network namespace": { + hostname: "test-hostname", + networkNs: runtime.NamespaceMode_POD, + expectedEnv: "HOSTNAME=test-hostname", + }, + "should add HOSTNAME=sandbox.Hostname for host network namespace": { + hostname: "test-hostname", + networkNs: runtime.NamespaceMode_NODE, + expectedEnv: "HOSTNAME=test-hostname", + }, + "should add HOSTNAME=os.Hostname for host network namespace if sandbox.Hostname is not set": { + hostname: "", + networkNs: runtime.NamespaceMode_NODE, + expectedEnv: "HOSTNAME=real-hostname", + }, + } { + t.Logf("TestCase %q", desc) + sandboxConfig.Hostname = test.hostname + sandboxConfig.Linux.SecurityContext = &runtime.LinuxSandboxSecurityContext{ + NamespaceOptions: &runtime.NamespaceOption{Network: test.networkNs}, + } + spec, err := c.containerSpec(testID, testSandboxID, testPid, "", testContainerName, containerConfig, sandboxConfig, imageConfig, nil, ociRuntime) + require.NoError(t, err) + specCheck(t, testID, testSandboxID, testPid, spec) + assert.Contains(t, spec.Process.Env, test.expectedEnv) + } +} + +func TestDisableCgroup(t *testing.T) { + containerConfig, sandboxConfig, imageConfig, _ := getCreateContainerTestData() + ociRuntime := config.Runtime{} + c := newTestCRIService() + c.config.DisableCgroup = true + spec, err := c.containerSpec("test-id", "sandbox-id", 1234, "", "container-name", containerConfig, sandboxConfig, imageConfig, nil, ociRuntime) + require.NoError(t, err) + + t.Log("resource limit should not be set") + assert.Nil(t, spec.Linux.Resources.Memory) + assert.Nil(t, spec.Linux.Resources.CPU) + + t.Log("cgroup path should be empty") + assert.Empty(t, spec.Linux.CgroupsPath) +} + +func TestGenerateUserString(t *testing.T) { + type testcase struct { + // the name of the test case + name string + + u string + uid, gid *runtime.Int64Value + + result string + expectedError bool + } + testcases := []testcase{ + { + name: "Empty", + result: "", + }, + { + name: "Username Only", + u: "testuser", + result: "testuser", + }, + { + name: "Username, UID", + u: "testuser", + uid: &runtime.Int64Value{Value: 1}, + result: "testuser", + }, + { + name: "Username, UID, GID", + u: "testuser", + uid: &runtime.Int64Value{Value: 1}, + gid: &runtime.Int64Value{Value: 10}, + result: "testuser:10", + }, + { + name: "Username, GID", + u: "testuser", + gid: &runtime.Int64Value{Value: 10}, + result: "testuser:10", + }, + { + name: "UID only", + uid: &runtime.Int64Value{Value: 1}, + result: "1", + }, + { + name: "UID, GID", + uid: &runtime.Int64Value{Value: 1}, + gid: &runtime.Int64Value{Value: 10}, + result: "1:10", + }, + { + name: "GID only", + gid: &runtime.Int64Value{Value: 10}, + result: "", + expectedError: true, + }, + } + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + r, err := generateUserString(tc.u, tc.uid, tc.gid) + if tc.expectedError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + assert.Equal(t, tc.result, r) + }) + } +} + +func TestPrivilegedDevices(t *testing.T) { + testPid := uint32(1234) + c := newTestCRIService() + testSandboxID := "sandbox-id" + testContainerName := "container-name" + containerConfig, sandboxConfig, imageConfig, _ := getCreateContainerTestData() + + for desc, test := range map[string]struct { + privileged bool + privilegedWithoutHostDevices bool + expectHostDevices bool + }{ + "expect no host devices when privileged is false": { + privileged: false, + privilegedWithoutHostDevices: false, + expectHostDevices: false, + }, + "expect no host devices when privileged is false and privilegedWithoutHostDevices is true": { + privileged: false, + privilegedWithoutHostDevices: true, + expectHostDevices: false, + }, + "expect host devices when privileged is true": { + privileged: true, + privilegedWithoutHostDevices: false, + expectHostDevices: true, + }, + "expect no host devices when privileged is true and privilegedWithoutHostDevices is true": { + privileged: true, + privilegedWithoutHostDevices: true, + expectHostDevices: false, + }, + } { + t.Logf("TestCase %q", desc) + + containerConfig.Linux.SecurityContext.Privileged = test.privileged + sandboxConfig.Linux.SecurityContext.Privileged = test.privileged + + ociRuntime := config.Runtime{ + PrivilegedWithoutHostDevices: test.privilegedWithoutHostDevices, + } + spec, err := c.containerSpec(t.Name(), testSandboxID, testPid, "", testContainerName, containerConfig, sandboxConfig, imageConfig, nil, ociRuntime) + assert.NoError(t, err) + + hostDevicesRaw, err := devices.HostDevices() + assert.NoError(t, err) + var hostDevices []*libcontainerconfigs.Device + for _, dev := range hostDevicesRaw { + // https://github.com/containerd/cri/pull/1521#issuecomment-652807951 + if dev.DeviceRule.Major != 0 { + hostDevices = append(hostDevices, dev) + } + } + + if test.expectHostDevices { + assert.Len(t, spec.Linux.Devices, len(hostDevices)) + } else { + assert.Empty(t, spec.Linux.Devices) + } + } +} + +func TestBaseOCISpec(t *testing.T) { + c := newTestCRIService() + baseLimit := int64(100) + c.baseOCISpecs = map[string]*oci.Spec{ + "/etc/containerd/cri-base.json": { + Process: &runtimespec.Process{ + User: runtimespec.User{AdditionalGids: []uint32{9999}}, + Capabilities: &runtimespec.LinuxCapabilities{ + Permitted: []string{"CAP_SETUID"}, + }, + }, + Linux: &runtimespec.Linux{ + Resources: &runtimespec.LinuxResources{ + Memory: &runtimespec.LinuxMemory{Limit: &baseLimit}, // Will be overwritten by `getCreateContainerTestData` + }, + }, + }, + } + + ociRuntime := config.Runtime{} + ociRuntime.BaseRuntimeSpec = "/etc/containerd/cri-base.json" + + testID := "test-id" + testSandboxID := "sandbox-id" + testContainerName := "container-name" + testPid := uint32(1234) + containerConfig, sandboxConfig, imageConfig, specCheck := getCreateContainerTestData() + + spec, err := c.containerSpec(testID, testSandboxID, testPid, "", testContainerName, containerConfig, sandboxConfig, imageConfig, nil, ociRuntime) + assert.NoError(t, err) + + specCheck(t, testID, testSandboxID, testPid, spec) + + assert.Contains(t, spec.Process.User.AdditionalGids, uint32(9999)) + assert.Len(t, spec.Process.User.AdditionalGids, 3) + + assert.Contains(t, spec.Process.Capabilities.Permitted, "CAP_SETUID") + assert.Len(t, spec.Process.Capabilities.Permitted, 1) + + assert.Equal(t, *spec.Linux.Resources.Memory.Limit, containerConfig.Linux.Resources.MemoryLimitInBytes) +} diff --git a/pkg/server/container_create_other.go b/pkg/server/container_create_other.go new file mode 100644 index 000000000..12e5ae95c --- /dev/null +++ b/pkg/server/container_create_other.go @@ -0,0 +1,44 @@ +// +build !windows,!linux + +/* + Copyright The containerd 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 + + http://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. +*/ + +package server + +import ( + "github.com/containerd/containerd/oci" + imagespec "github.com/opencontainers/image-spec/specs-go/v1" + runtimespec "github.com/opencontainers/runtime-spec/specs-go" + runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" + + "github.com/containerd/containerd/pkg/config" +) + +// containerMounts sets up necessary container system file mounts +// including /dev/shm, /etc/hosts and /etc/resolv.conf. +func (c *criService) containerMounts(sandboxID string, config *runtime.ContainerConfig) []*runtime.Mount { + return []*runtime.Mount{} +} + +func (c *criService) containerSpec(id string, sandboxID string, sandboxPid uint32, netNSPath string, containerName string, + config *runtime.ContainerConfig, sandboxConfig *runtime.PodSandboxConfig, imageConfig *imagespec.ImageConfig, + extraMounts []*runtime.Mount, ociRuntime config.Runtime) (_ *runtimespec.Spec, retErr error) { + return c.runtimeSpec(id, ociRuntime.BaseRuntimeSpec) +} + +func (c *criService) containerSpecOpts(config *runtime.ContainerConfig, imageConfig *imagespec.ImageConfig) ([]oci.SpecOpts, error) { + return []oci.SpecOpts{}, nil +} diff --git a/pkg/server/container_create_other_test.go b/pkg/server/container_create_other_test.go new file mode 100644 index 000000000..891c46765 --- /dev/null +++ b/pkg/server/container_create_other_test.go @@ -0,0 +1,40 @@ +// +build !windows,!linux + +/* + Copyright The containerd 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 + + http://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. +*/ + +package server + +import ( + "testing" + + imagespec "github.com/opencontainers/image-spec/specs-go/v1" + runtimespec "github.com/opencontainers/runtime-spec/specs-go" + runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" +) + +// checkMount is defined by all tests but not used here +var _ = checkMount + +func getCreateContainerTestData() (*runtime.ContainerConfig, *runtime.PodSandboxConfig, + *imagespec.ImageConfig, func(*testing.T, string, string, uint32, *runtimespec.Spec)) { + config := &runtime.ContainerConfig{} + sandboxConfig := &runtime.PodSandboxConfig{} + imageConfig := &imagespec.ImageConfig{} + specCheck := func(t *testing.T, id string, sandboxID string, sandboxPid uint32, spec *runtimespec.Spec) { + } + return config, sandboxConfig, imageConfig, specCheck +} diff --git a/pkg/server/container_create_test.go b/pkg/server/container_create_test.go new file mode 100644 index 000000000..3fadd48f2 --- /dev/null +++ b/pkg/server/container_create_test.go @@ -0,0 +1,407 @@ +/* + Copyright The containerd 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 + + http://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. +*/ + +package server + +import ( + "context" + "path/filepath" + "testing" + + "github.com/containerd/containerd/oci" + imagespec "github.com/opencontainers/image-spec/specs-go/v1" + runtimespec "github.com/opencontainers/runtime-spec/specs-go" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" + + "github.com/containerd/containerd/pkg/config" + "github.com/containerd/containerd/pkg/constants" + "github.com/containerd/containerd/pkg/containerd/opts" +) + +func checkMount(t *testing.T, mounts []runtimespec.Mount, src, dest, typ string, + contains, notcontains []string) { + found := false + for _, m := range mounts { + if m.Source == src && m.Destination == dest { + assert.Equal(t, m.Type, typ) + for _, c := range contains { + assert.Contains(t, m.Options, c) + } + for _, n := range notcontains { + assert.NotContains(t, m.Options, n) + } + found = true + break + } + } + assert.True(t, found, "mount from %q to %q not found", src, dest) +} + +func TestGeneralContainerSpec(t *testing.T) { + testID := "test-id" + testPid := uint32(1234) + containerConfig, sandboxConfig, imageConfig, specCheck := getCreateContainerTestData() + ociRuntime := config.Runtime{} + c := newTestCRIService() + testSandboxID := "sandbox-id" + testContainerName := "container-name" + spec, err := c.containerSpec(testID, testSandboxID, testPid, "", testContainerName, containerConfig, sandboxConfig, imageConfig, nil, ociRuntime) + require.NoError(t, err) + specCheck(t, testID, testSandboxID, testPid, spec) +} + +func TestPodAnnotationPassthroughContainerSpec(t *testing.T) { + testID := "test-id" + testSandboxID := "sandbox-id" + testContainerName := "container-name" + testPid := uint32(1234) + + for desc, test := range map[string]struct { + podAnnotations []string + configChange func(*runtime.PodSandboxConfig) + specCheck func(*testing.T, *runtimespec.Spec) + }{ + "a passthrough annotation should be passed as an OCI annotation": { + podAnnotations: []string{"c"}, + specCheck: func(t *testing.T, spec *runtimespec.Spec) { + assert.Equal(t, spec.Annotations["c"], "d") + }, + }, + "a non-passthrough annotation should not be passed as an OCI annotation": { + configChange: func(c *runtime.PodSandboxConfig) { + c.Annotations["d"] = "e" + }, + podAnnotations: []string{"c"}, + specCheck: func(t *testing.T, spec *runtimespec.Spec) { + assert.Equal(t, spec.Annotations["c"], "d") + _, ok := spec.Annotations["d"] + assert.False(t, ok) + }, + }, + "passthrough annotations should support wildcard match": { + configChange: func(c *runtime.PodSandboxConfig) { + c.Annotations["t.f"] = "j" + c.Annotations["z.g"] = "o" + c.Annotations["z"] = "o" + c.Annotations["y.ca"] = "b" + c.Annotations["y"] = "b" + }, + podAnnotations: []string{"t*", "z.*", "y.c*"}, + specCheck: func(t *testing.T, spec *runtimespec.Spec) { + t.Logf("%+v", spec.Annotations) + assert.Equal(t, spec.Annotations["t.f"], "j") + assert.Equal(t, spec.Annotations["z.g"], "o") + assert.Equal(t, spec.Annotations["y.ca"], "b") + _, ok := spec.Annotations["y"] + assert.False(t, ok) + _, ok = spec.Annotations["z"] + assert.False(t, ok) + }, + }, + } { + t.Run(desc, func(t *testing.T) { + c := newTestCRIService() + containerConfig, sandboxConfig, imageConfig, specCheck := getCreateContainerTestData() + if test.configChange != nil { + test.configChange(sandboxConfig) + } + + ociRuntime := config.Runtime{ + PodAnnotations: test.podAnnotations, + } + spec, err := c.containerSpec(testID, testSandboxID, testPid, "", testContainerName, + containerConfig, sandboxConfig, imageConfig, nil, ociRuntime) + assert.NoError(t, err) + assert.NotNil(t, spec) + specCheck(t, testID, testSandboxID, testPid, spec) + if test.specCheck != nil { + test.specCheck(t, spec) + } + }) + } +} + +func TestContainerSpecCommand(t *testing.T) { + for desc, test := range map[string]struct { + criEntrypoint []string + criArgs []string + imageEntrypoint []string + imageArgs []string + expected []string + expectErr bool + }{ + "should use cri entrypoint if it's specified": { + criEntrypoint: []string{"a", "b"}, + imageEntrypoint: []string{"c", "d"}, + imageArgs: []string{"e", "f"}, + expected: []string{"a", "b"}, + }, + "should use cri entrypoint if it's specified even if it's empty": { + criEntrypoint: []string{}, + criArgs: []string{"a", "b"}, + imageEntrypoint: []string{"c", "d"}, + imageArgs: []string{"e", "f"}, + expected: []string{"a", "b"}, + }, + "should use cri entrypoint and args if they are specified": { + criEntrypoint: []string{"a", "b"}, + criArgs: []string{"c", "d"}, + imageEntrypoint: []string{"e", "f"}, + imageArgs: []string{"g", "h"}, + expected: []string{"a", "b", "c", "d"}, + }, + "should use image entrypoint if cri entrypoint is not specified": { + criArgs: []string{"a", "b"}, + imageEntrypoint: []string{"c", "d"}, + imageArgs: []string{"e", "f"}, + expected: []string{"c", "d", "a", "b"}, + }, + "should use image args if both cri entrypoint and args are not specified": { + imageEntrypoint: []string{"c", "d"}, + imageArgs: []string{"e", "f"}, + expected: []string{"c", "d", "e", "f"}, + }, + "should return error if both entrypoint and args are empty": { + expectErr: true, + }, + } { + + config, _, imageConfig, _ := getCreateContainerTestData() + config.Command = test.criEntrypoint + config.Args = test.criArgs + imageConfig.Entrypoint = test.imageEntrypoint + imageConfig.Cmd = test.imageArgs + + var spec runtimespec.Spec + err := opts.WithProcessArgs(config, imageConfig)(context.Background(), nil, nil, &spec) + if test.expectErr { + assert.Error(t, err) + continue + } + assert.NoError(t, err) + assert.Equal(t, test.expected, spec.Process.Args, desc) + } +} + +func TestVolumeMounts(t *testing.T) { + testContainerRootDir := "test-container-root" + for desc, test := range map[string]struct { + criMounts []*runtime.Mount + imageVolumes map[string]struct{} + expectedMountDest []string + }{ + "should setup rw mount for image volumes": { + imageVolumes: map[string]struct{}{ + "/test-volume-1": {}, + "/test-volume-2": {}, + }, + expectedMountDest: []string{ + "/test-volume-1", + "/test-volume-2", + }, + }, + "should skip image volumes if already mounted by CRI": { + criMounts: []*runtime.Mount{ + { + ContainerPath: "/test-volume-1", + HostPath: "/test-hostpath-1", + }, + }, + imageVolumes: map[string]struct{}{ + "/test-volume-1": {}, + "/test-volume-2": {}, + }, + expectedMountDest: []string{ + "/test-volume-2", + }, + }, + "should compare and return cleanpath": { + criMounts: []*runtime.Mount{ + { + ContainerPath: "/test-volume-1", + HostPath: "/test-hostpath-1", + }, + }, + imageVolumes: map[string]struct{}{ + "/test-volume-1/": {}, + "/test-volume-2/": {}, + }, + expectedMountDest: []string{ + "/test-volume-2/", + }, + }, + } { + t.Logf("TestCase %q", desc) + config := &imagespec.ImageConfig{ + Volumes: test.imageVolumes, + } + c := newTestCRIService() + got := c.volumeMounts(testContainerRootDir, test.criMounts, config) + assert.Len(t, got, len(test.expectedMountDest)) + for _, dest := range test.expectedMountDest { + found := false + for _, m := range got { + if m.ContainerPath == dest { + found = true + assert.Equal(t, + filepath.Dir(m.HostPath), + filepath.Join(testContainerRootDir, "volumes")) + break + } + } + assert.True(t, found) + } + } +} + +func TestContainerAnnotationPassthroughContainerSpec(t *testing.T) { + testID := "test-id" + testSandboxID := "sandbox-id" + testContainerName := "container-name" + testPid := uint32(1234) + + for desc, test := range map[string]struct { + podAnnotations []string + containerAnnotations []string + podConfigChange func(*runtime.PodSandboxConfig) + configChange func(*runtime.ContainerConfig) + specCheck func(*testing.T, *runtimespec.Spec) + }{ + "passthrough annotations from pod and container should be passed as an OCI annotation": { + podConfigChange: func(p *runtime.PodSandboxConfig) { + p.Annotations["pod.annotation.1"] = "1" + p.Annotations["pod.annotation.2"] = "2" + p.Annotations["pod.annotation.3"] = "3" + }, + configChange: func(c *runtime.ContainerConfig) { + c.Annotations["container.annotation.1"] = "1" + c.Annotations["container.annotation.2"] = "2" + c.Annotations["container.annotation.3"] = "3" + }, + podAnnotations: []string{"pod.annotation.1"}, + containerAnnotations: []string{"container.annotation.1"}, + specCheck: func(t *testing.T, spec *runtimespec.Spec) { + assert.Equal(t, "1", spec.Annotations["container.annotation.1"]) + _, ok := spec.Annotations["container.annotation.2"] + assert.False(t, ok) + _, ok = spec.Annotations["container.annotation.3"] + assert.False(t, ok) + assert.Equal(t, "1", spec.Annotations["pod.annotation.1"]) + _, ok = spec.Annotations["pod.annotation.2"] + assert.False(t, ok) + _, ok = spec.Annotations["pod.annotation.3"] + assert.False(t, ok) + }, + }, + "passthrough annotations from pod and container should support wildcard": { + podConfigChange: func(p *runtime.PodSandboxConfig) { + p.Annotations["pod.annotation.1"] = "1" + p.Annotations["pod.annotation.2"] = "2" + p.Annotations["pod.annotation.3"] = "3" + }, + configChange: func(c *runtime.ContainerConfig) { + c.Annotations["container.annotation.1"] = "1" + c.Annotations["container.annotation.2"] = "2" + c.Annotations["container.annotation.3"] = "3" + }, + podAnnotations: []string{"pod.annotation.*"}, + containerAnnotations: []string{"container.annotation.*"}, + specCheck: func(t *testing.T, spec *runtimespec.Spec) { + assert.Equal(t, "1", spec.Annotations["container.annotation.1"]) + assert.Equal(t, "2", spec.Annotations["container.annotation.2"]) + assert.Equal(t, "3", spec.Annotations["container.annotation.3"]) + assert.Equal(t, "1", spec.Annotations["pod.annotation.1"]) + assert.Equal(t, "2", spec.Annotations["pod.annotation.2"]) + assert.Equal(t, "3", spec.Annotations["pod.annotation.3"]) + }, + }, + "annotations should not pass through if no passthrough annotations are configured": { + podConfigChange: func(p *runtime.PodSandboxConfig) { + p.Annotations["pod.annotation.1"] = "1" + p.Annotations["pod.annotation.2"] = "2" + p.Annotations["pod.annotation.3"] = "3" + }, + configChange: func(c *runtime.ContainerConfig) { + c.Annotations["container.annotation.1"] = "1" + c.Annotations["container.annotation.2"] = "2" + c.Annotations["container.annotation.3"] = "3" + }, + podAnnotations: []string{}, + containerAnnotations: []string{}, + specCheck: func(t *testing.T, spec *runtimespec.Spec) { + _, ok := spec.Annotations["container.annotation.1"] + assert.False(t, ok) + _, ok = spec.Annotations["container.annotation.2"] + assert.False(t, ok) + _, ok = spec.Annotations["container.annotation.3"] + assert.False(t, ok) + _, ok = spec.Annotations["pod.annotation.1"] + assert.False(t, ok) + _, ok = spec.Annotations["pod.annotation.2"] + assert.False(t, ok) + _, ok = spec.Annotations["pod.annotation.3"] + assert.False(t, ok) + }, + }, + } { + t.Run(desc, func(t *testing.T) { + c := newTestCRIService() + containerConfig, sandboxConfig, imageConfig, specCheck := getCreateContainerTestData() + if test.configChange != nil { + test.configChange(containerConfig) + } + if test.podConfigChange != nil { + test.podConfigChange(sandboxConfig) + } + ociRuntime := config.Runtime{ + PodAnnotations: test.podAnnotations, + ContainerAnnotations: test.containerAnnotations, + } + spec, err := c.containerSpec(testID, testSandboxID, testPid, "", testContainerName, + containerConfig, sandboxConfig, imageConfig, nil, ociRuntime) + assert.NoError(t, err) + assert.NotNil(t, spec) + specCheck(t, testID, testSandboxID, testPid, spec) + if test.specCheck != nil { + test.specCheck(t, spec) + } + }) + } +} + +func TestBaseRuntimeSpec(t *testing.T) { + c := newTestCRIService() + c.baseOCISpecs = map[string]*oci.Spec{ + "/etc/containerd/cri-base.json": { + Version: "1.0.2", + Hostname: "old", + }, + } + + out, err := c.runtimeSpec("id1", "/etc/containerd/cri-base.json", oci.WithHostname("new")) + assert.NoError(t, err) + + assert.Equal(t, "1.0.2", out.Version) + assert.Equal(t, "new", out.Hostname) + + // Make sure original base spec not changed + assert.NotEqual(t, out, c.baseOCISpecs["/etc/containerd/cri-base.json"]) + assert.Equal(t, c.baseOCISpecs["/etc/containerd/cri-base.json"].Hostname, "old") + + assert.Equal(t, filepath.Join("/", constants.K8sContainerdNamespace, "id1"), out.Linux.CgroupsPath) +} diff --git a/vendor/github.com/containerd/cri/pkg/server/container_create_windows.go b/pkg/server/container_create_windows.go similarity index 96% rename from vendor/github.com/containerd/cri/pkg/server/container_create_windows.go rename to pkg/server/container_create_windows.go index 86a08d89e..f41447650 100644 --- a/vendor/github.com/containerd/cri/pkg/server/container_create_windows.go +++ b/pkg/server/container_create_windows.go @@ -24,9 +24,9 @@ import ( runtimespec "github.com/opencontainers/runtime-spec/specs-go" runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" - "github.com/containerd/cri/pkg/annotations" - "github.com/containerd/cri/pkg/config" - customopts "github.com/containerd/cri/pkg/containerd/opts" + "github.com/containerd/containerd/pkg/annotations" + "github.com/containerd/containerd/pkg/config" + customopts "github.com/containerd/containerd/pkg/containerd/opts" ) // No container mounts for windows. diff --git a/pkg/server/container_create_windows_test.go b/pkg/server/container_create_windows_test.go new file mode 100644 index 000000000..8de431f5d --- /dev/null +++ b/pkg/server/container_create_windows_test.go @@ -0,0 +1,189 @@ +// +build windows + +/* + Copyright The containerd 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 + + http://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. +*/ + +package server + +import ( + "testing" + + imagespec "github.com/opencontainers/image-spec/specs-go/v1" + runtimespec "github.com/opencontainers/runtime-spec/specs-go" + "github.com/stretchr/testify/assert" + runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" + + "github.com/containerd/containerd/pkg/annotations" + "github.com/containerd/containerd/pkg/config" +) + +func getCreateContainerTestData() (*runtime.ContainerConfig, *runtime.PodSandboxConfig, + *imagespec.ImageConfig, func(*testing.T, string, string, uint32, *runtimespec.Spec)) { + config := &runtime.ContainerConfig{ + Metadata: &runtime.ContainerMetadata{ + Name: "test-name", + Attempt: 1, + }, + Image: &runtime.ImageSpec{ + Image: "sha256:c75bebcdd211f41b3a460c7bf82970ed6c75acaab9cd4c9a4e125b03ca113799", + }, + Command: []string{"test", "command"}, + Args: []string{"test", "args"}, + WorkingDir: "test-cwd", + Envs: []*runtime.KeyValue{ + {Key: "k1", Value: "v1"}, + {Key: "k2", Value: "v2"}, + {Key: "k3", Value: "v3=v3bis"}, + {Key: "k4", Value: "v4=v4bis=foop"}, + }, + Mounts: []*runtime.Mount{ + // everything default + { + ContainerPath: "container-path-1", + HostPath: "host-path-1", + }, + // readOnly + { + ContainerPath: "container-path-2", + HostPath: "host-path-2", + Readonly: true, + }, + }, + Labels: map[string]string{"a": "b"}, + Annotations: map[string]string{"c": "d"}, + Windows: &runtime.WindowsContainerConfig{ + Resources: &runtime.WindowsContainerResources{ + CpuShares: 100, + CpuCount: 200, + CpuMaximum: 300, + MemoryLimitInBytes: 400, + }, + SecurityContext: &runtime.WindowsContainerSecurityContext{ + RunAsUsername: "test-user", + CredentialSpec: "{\"test\": \"spec\"}", + }, + }, + } + sandboxConfig := &runtime.PodSandboxConfig{ + Metadata: &runtime.PodSandboxMetadata{ + Name: "test-sandbox-name", + Uid: "test-sandbox-uid", + Namespace: "test-sandbox-ns", + Attempt: 2, + }, + Hostname: "test-hostname", + Annotations: map[string]string{"c": "d"}, + } + imageConfig := &imagespec.ImageConfig{ + Env: []string{"ik1=iv1", "ik2=iv2", "ik3=iv3=iv3bis", "ik4=iv4=iv4bis=boop"}, + Entrypoint: []string{"/entrypoint"}, + Cmd: []string{"cmd"}, + WorkingDir: "/workspace", + User: "ContainerUser", + } + specCheck := func(t *testing.T, id string, sandboxID string, sandboxPid uint32, spec *runtimespec.Spec) { + assert.Nil(t, spec.Root) + assert.Equal(t, "test-hostname", spec.Hostname) + assert.Equal(t, []string{"test", "command", "test", "args"}, spec.Process.Args) + assert.Equal(t, "test-cwd", spec.Process.Cwd) + assert.Contains(t, spec.Process.Env, "k1=v1", "k2=v2", "k3=v3=v3bis", "ik4=iv4=iv4bis=boop") + assert.Contains(t, spec.Process.Env, "ik1=iv1", "ik2=iv2", "ik3=iv3=iv3bis", "k4=v4=v4bis=foop") + + t.Logf("Check bind mount") + checkMount(t, spec.Mounts, "host-path-1", "container-path-1", "", []string{"rw"}, nil) + checkMount(t, spec.Mounts, "host-path-2", "container-path-2", "", []string{"ro"}, nil) + + t.Logf("Check resource limits") + assert.EqualValues(t, *spec.Windows.Resources.CPU.Shares, 100) + assert.EqualValues(t, *spec.Windows.Resources.CPU.Count, 200) + assert.EqualValues(t, *spec.Windows.Resources.CPU.Maximum, 300) + assert.EqualValues(t, *spec.Windows.Resources.CPU.Maximum, 300) + assert.EqualValues(t, *spec.Windows.Resources.Memory.Limit, 400) + + // Also checks if override of the image configs user is behaving. + t.Logf("Check username") + assert.Contains(t, spec.Process.User.Username, "test-user") + + t.Logf("Check credential spec") + assert.Contains(t, spec.Windows.CredentialSpec, "{\"test\": \"spec\"}") + + t.Logf("Check PodSandbox annotations") + assert.Contains(t, spec.Annotations, annotations.SandboxID) + assert.EqualValues(t, spec.Annotations[annotations.SandboxID], sandboxID) + + assert.Contains(t, spec.Annotations, annotations.ContainerType) + assert.EqualValues(t, spec.Annotations[annotations.ContainerType], annotations.ContainerTypeContainer) + } + return config, sandboxConfig, imageConfig, specCheck +} + +func TestContainerWindowsNetworkNamespace(t *testing.T) { + testID := "test-id" + testSandboxID := "sandbox-id" + testContainerName := "container-name" + testPid := uint32(1234) + nsPath := "test-cni" + c := newTestCRIService() + + containerConfig, sandboxConfig, imageConfig, specCheck := getCreateContainerTestData() + spec, err := c.containerSpec(testID, testSandboxID, testPid, nsPath, testContainerName, containerConfig, sandboxConfig, imageConfig, nil, config.Runtime{}) + assert.NoError(t, err) + assert.NotNil(t, spec) + specCheck(t, testID, testSandboxID, testPid, spec) + assert.NotNil(t, spec.Windows) + assert.NotNil(t, spec.Windows.Network) + assert.Equal(t, nsPath, spec.Windows.Network.NetworkNamespace) +} + +func TestMountCleanPath(t *testing.T) { + testID := "test-id" + testSandboxID := "sandbox-id" + testContainerName := "container-name" + testPid := uint32(1234) + nsPath := "test-cni" + c := newTestCRIService() + + containerConfig, sandboxConfig, imageConfig, specCheck := getCreateContainerTestData() + containerConfig.Mounts = append(containerConfig.Mounts, &runtime.Mount{ + ContainerPath: "c:/test/container-path", + HostPath: "c:/test/host-path", + }) + spec, err := c.containerSpec(testID, testSandboxID, testPid, nsPath, testContainerName, containerConfig, sandboxConfig, imageConfig, nil, config.Runtime{}) + assert.NoError(t, err) + assert.NotNil(t, spec) + specCheck(t, testID, testSandboxID, testPid, spec) + checkMount(t, spec.Mounts, "c:\\test\\host-path", "c:\\test\\container-path", "", []string{"rw"}, nil) +} + +func TestMountNamedPipe(t *testing.T) { + testID := "test-id" + testSandboxID := "sandbox-id" + testContainerName := "container-name" + testPid := uint32(1234) + nsPath := "test-cni" + c := newTestCRIService() + + containerConfig, sandboxConfig, imageConfig, specCheck := getCreateContainerTestData() + containerConfig.Mounts = append(containerConfig.Mounts, &runtime.Mount{ + ContainerPath: `\\.\pipe\foo`, + HostPath: `\\.\pipe\foo`, + }) + spec, err := c.containerSpec(testID, testSandboxID, testPid, nsPath, testContainerName, containerConfig, sandboxConfig, imageConfig, nil, config.Runtime{}) + assert.NoError(t, err) + assert.NotNil(t, spec) + specCheck(t, testID, testSandboxID, testPid, spec) + checkMount(t, spec.Mounts, `\\.\pipe\foo`, `\\.\pipe\foo`, "", []string{"rw"}, nil) +} diff --git a/vendor/github.com/containerd/cri/pkg/server/container_exec.go b/pkg/server/container_exec.go similarity index 100% rename from vendor/github.com/containerd/cri/pkg/server/container_exec.go rename to pkg/server/container_exec.go diff --git a/vendor/github.com/containerd/cri/pkg/server/container_execsync.go b/pkg/server/container_execsync.go similarity index 97% rename from vendor/github.com/containerd/cri/pkg/server/container_execsync.go rename to pkg/server/container_execsync.go index 1c019f651..7397d7055 100644 --- a/vendor/github.com/containerd/cri/pkg/server/container_execsync.go +++ b/pkg/server/container_execsync.go @@ -32,10 +32,10 @@ import ( "k8s.io/client-go/tools/remotecommand" runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" - ctrdutil "github.com/containerd/cri/pkg/containerd/util" - cioutil "github.com/containerd/cri/pkg/ioutil" - cio "github.com/containerd/cri/pkg/server/io" - "github.com/containerd/cri/pkg/util" + ctrdutil "github.com/containerd/containerd/pkg/containerd/util" + cioutil "github.com/containerd/containerd/pkg/ioutil" + cio "github.com/containerd/containerd/pkg/server/io" + "github.com/containerd/containerd/pkg/util" ) // ExecSync executes a command in the container, and returns the stdout output. diff --git a/vendor/github.com/containerd/cri/pkg/server/container_list.go b/pkg/server/container_list.go similarity index 97% rename from vendor/github.com/containerd/cri/pkg/server/container_list.go rename to pkg/server/container_list.go index c9e88d13d..c27e231df 100644 --- a/vendor/github.com/containerd/cri/pkg/server/container_list.go +++ b/pkg/server/container_list.go @@ -21,7 +21,7 @@ import ( runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" - containerstore "github.com/containerd/cri/pkg/store/container" + containerstore "github.com/containerd/containerd/pkg/store/container" ) // ListContainers lists all containers matching the filter. diff --git a/pkg/server/container_list_test.go b/pkg/server/container_list_test.go new file mode 100644 index 000000000..a80d516a7 --- /dev/null +++ b/pkg/server/container_list_test.go @@ -0,0 +1,345 @@ +/* + Copyright The containerd 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 + + http://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. +*/ + +package server + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/net/context" + runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" + + containerstore "github.com/containerd/containerd/pkg/store/container" + sandboxstore "github.com/containerd/containerd/pkg/store/sandbox" +) + +func TestToCRIContainer(t *testing.T) { + config := &runtime.ContainerConfig{ + Metadata: &runtime.ContainerMetadata{ + Name: "test-name", + Attempt: 1, + }, + Image: &runtime.ImageSpec{Image: "test-image"}, + Labels: map[string]string{"a": "b"}, + Annotations: map[string]string{"c": "d"}, + } + createdAt := time.Now().UnixNano() + container, err := containerstore.NewContainer( + containerstore.Metadata{ + ID: "test-id", + Name: "test-name", + SandboxID: "test-sandbox-id", + Config: config, + ImageRef: "test-image-ref", + }, + containerstore.WithFakeStatus( + containerstore.Status{ + Pid: 1234, + CreatedAt: createdAt, + StartedAt: time.Now().UnixNano(), + FinishedAt: time.Now().UnixNano(), + ExitCode: 1, + Reason: "test-reason", + Message: "test-message", + }, + ), + ) + assert.NoError(t, err) + expect := &runtime.Container{ + Id: "test-id", + PodSandboxId: "test-sandbox-id", + Metadata: config.GetMetadata(), + Image: config.GetImage(), + ImageRef: "test-image-ref", + State: runtime.ContainerState_CONTAINER_EXITED, + CreatedAt: createdAt, + Labels: config.GetLabels(), + Annotations: config.GetAnnotations(), + } + c := toCRIContainer(container) + assert.Equal(t, expect, c) +} + +func TestFilterContainers(t *testing.T) { + c := newTestCRIService() + + testContainers := []*runtime.Container{ + { + Id: "1", + PodSandboxId: "s-1", + Metadata: &runtime.ContainerMetadata{Name: "name-1", Attempt: 1}, + State: runtime.ContainerState_CONTAINER_RUNNING, + }, + { + Id: "2", + PodSandboxId: "s-2", + Metadata: &runtime.ContainerMetadata{Name: "name-2", Attempt: 2}, + State: runtime.ContainerState_CONTAINER_EXITED, + Labels: map[string]string{"a": "b"}, + }, + { + Id: "3", + PodSandboxId: "s-2", + Metadata: &runtime.ContainerMetadata{Name: "name-2", Attempt: 3}, + State: runtime.ContainerState_CONTAINER_CREATED, + Labels: map[string]string{"c": "d"}, + }, + } + for desc, test := range map[string]struct { + filter *runtime.ContainerFilter + expect []*runtime.Container + }{ + "no filter": { + expect: testContainers, + }, + "id filter": { + filter: &runtime.ContainerFilter{Id: "2"}, + expect: []*runtime.Container{testContainers[1]}, + }, + "state filter": { + filter: &runtime.ContainerFilter{ + State: &runtime.ContainerStateValue{ + State: runtime.ContainerState_CONTAINER_EXITED, + }, + }, + expect: []*runtime.Container{testContainers[1]}, + }, + "label filter": { + filter: &runtime.ContainerFilter{ + LabelSelector: map[string]string{"a": "b"}, + }, + expect: []*runtime.Container{testContainers[1]}, + }, + "sandbox id filter": { + filter: &runtime.ContainerFilter{PodSandboxId: "s-2"}, + expect: []*runtime.Container{testContainers[1], testContainers[2]}, + }, + "mixed filter not matched": { + filter: &runtime.ContainerFilter{ + Id: "1", + PodSandboxId: "s-2", + LabelSelector: map[string]string{"a": "b"}, + }, + expect: []*runtime.Container{}, + }, + "mixed filter matched": { + filter: &runtime.ContainerFilter{ + PodSandboxId: "s-2", + State: &runtime.ContainerStateValue{ + State: runtime.ContainerState_CONTAINER_CREATED, + }, + LabelSelector: map[string]string{"c": "d"}, + }, + expect: []*runtime.Container{testContainers[2]}, + }, + } { + filtered := c.filterCRIContainers(testContainers, test.filter) + assert.Equal(t, test.expect, filtered, desc) + } +} + +// containerForTest is a helper type for test. +type containerForTest struct { + metadata containerstore.Metadata + status containerstore.Status +} + +func (c containerForTest) toContainer() (containerstore.Container, error) { + return containerstore.NewContainer( + c.metadata, + containerstore.WithFakeStatus(c.status), + ) +} + +func TestListContainers(t *testing.T) { + c := newTestCRIService() + sandboxesInStore := []sandboxstore.Sandbox{ + sandboxstore.NewSandbox( + sandboxstore.Metadata{ + ID: "s-1abcdef1234", + Name: "sandboxname-1", + Config: &runtime.PodSandboxConfig{Metadata: &runtime.PodSandboxMetadata{Name: "podname-1"}}, + }, + sandboxstore.Status{ + State: sandboxstore.StateReady, + }, + ), + sandboxstore.NewSandbox( + sandboxstore.Metadata{ + ID: "s-2abcdef1234", + Name: "sandboxname-2", + Config: &runtime.PodSandboxConfig{Metadata: &runtime.PodSandboxMetadata{Name: "podname-2"}}, + }, + sandboxstore.Status{ + State: sandboxstore.StateNotReady, + }, + ), + } + createdAt := time.Now().UnixNano() + startedAt := time.Now().UnixNano() + finishedAt := time.Now().UnixNano() + containersInStore := []containerForTest{ + { + metadata: containerstore.Metadata{ + ID: "c-1container", + Name: "name-1", + SandboxID: "s-1abcdef1234", + Config: &runtime.ContainerConfig{Metadata: &runtime.ContainerMetadata{Name: "name-1"}}, + }, + status: containerstore.Status{CreatedAt: createdAt}, + }, + { + metadata: containerstore.Metadata{ + ID: "c-2container", + Name: "name-2", + SandboxID: "s-1abcdef1234", + Config: &runtime.ContainerConfig{Metadata: &runtime.ContainerMetadata{Name: "name-2"}}, + }, + status: containerstore.Status{ + CreatedAt: createdAt, + StartedAt: startedAt, + }, + }, + { + metadata: containerstore.Metadata{ + ID: "c-3container", + Name: "name-3", + SandboxID: "s-1abcdef1234", + Config: &runtime.ContainerConfig{Metadata: &runtime.ContainerMetadata{Name: "name-3"}}, + }, + status: containerstore.Status{ + CreatedAt: createdAt, + StartedAt: startedAt, + FinishedAt: finishedAt, + }, + }, + { + metadata: containerstore.Metadata{ + ID: "c-4container", + Name: "name-4", + SandboxID: "s-2abcdef1234", + Config: &runtime.ContainerConfig{Metadata: &runtime.ContainerMetadata{Name: "name-4"}}, + }, + status: containerstore.Status{ + CreatedAt: createdAt, + }, + }, + } + + expectedContainers := []*runtime.Container{ + { + Id: "c-1container", + PodSandboxId: "s-1abcdef1234", + Metadata: &runtime.ContainerMetadata{Name: "name-1"}, + State: runtime.ContainerState_CONTAINER_CREATED, + CreatedAt: createdAt, + }, + { + Id: "c-2container", + PodSandboxId: "s-1abcdef1234", + Metadata: &runtime.ContainerMetadata{Name: "name-2"}, + State: runtime.ContainerState_CONTAINER_RUNNING, + CreatedAt: createdAt, + }, + { + Id: "c-3container", + PodSandboxId: "s-1abcdef1234", + Metadata: &runtime.ContainerMetadata{Name: "name-3"}, + State: runtime.ContainerState_CONTAINER_EXITED, + CreatedAt: createdAt, + }, + { + Id: "c-4container", + PodSandboxId: "s-2abcdef1234", + Metadata: &runtime.ContainerMetadata{Name: "name-4"}, + State: runtime.ContainerState_CONTAINER_CREATED, + CreatedAt: createdAt, + }, + } + + // Inject test sandbox metadata + for _, sb := range sandboxesInStore { + assert.NoError(t, c.sandboxStore.Add(sb)) + } + + // Inject test container metadata + for _, cntr := range containersInStore { + container, err := cntr.toContainer() + assert.NoError(t, err) + assert.NoError(t, c.containerStore.Add(container)) + } + + for testdesc, testdata := range map[string]struct { + filter *runtime.ContainerFilter + expect []*runtime.Container + }{ + "test without filter": { + filter: &runtime.ContainerFilter{}, + expect: expectedContainers, + }, + "test filter by sandboxid": { + filter: &runtime.ContainerFilter{ + PodSandboxId: "s-1abcdef1234", + }, + expect: expectedContainers[:3], + }, + "test filter by truncated sandboxid": { + filter: &runtime.ContainerFilter{ + PodSandboxId: "s-1", + }, + expect: expectedContainers[:3], + }, + "test filter by containerid": { + filter: &runtime.ContainerFilter{ + Id: "c-1container", + }, + expect: expectedContainers[:1], + }, + "test filter by truncated containerid": { + filter: &runtime.ContainerFilter{ + Id: "c-1", + }, + expect: expectedContainers[:1], + }, + "test filter by containerid and sandboxid": { + filter: &runtime.ContainerFilter{ + Id: "c-1container", + PodSandboxId: "s-1abcdef1234", + }, + expect: expectedContainers[:1], + }, + "test filter by truncated containerid and truncated sandboxid": { + filter: &runtime.ContainerFilter{ + Id: "c-1", + PodSandboxId: "s-1", + }, + expect: expectedContainers[:1], + }, + } { + t.Logf("TestCase: %s", testdesc) + resp, err := c.ListContainers(context.Background(), &runtime.ListContainersRequest{Filter: testdata.filter}) + assert.NoError(t, err) + require.NotNil(t, resp) + containers := resp.GetContainers() + assert.Len(t, containers, len(testdata.expect)) + for _, cntr := range testdata.expect { + assert.Contains(t, containers, cntr) + } + } +} diff --git a/vendor/github.com/containerd/cri/pkg/server/container_log_reopen.go b/pkg/server/container_log_reopen.go similarity index 100% rename from vendor/github.com/containerd/cri/pkg/server/container_log_reopen.go rename to pkg/server/container_log_reopen.go diff --git a/vendor/github.com/containerd/cri/pkg/server/container_remove.go b/pkg/server/container_remove.go similarity index 97% rename from vendor/github.com/containerd/cri/pkg/server/container_remove.go rename to pkg/server/container_remove.go index 6426635dd..15f982d2f 100644 --- a/vendor/github.com/containerd/cri/pkg/server/container_remove.go +++ b/pkg/server/container_remove.go @@ -25,8 +25,8 @@ import ( "golang.org/x/net/context" runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" - "github.com/containerd/cri/pkg/store" - containerstore "github.com/containerd/cri/pkg/store/container" + "github.com/containerd/containerd/pkg/store" + containerstore "github.com/containerd/containerd/pkg/store/container" ) // RemoveContainer removes the container. diff --git a/pkg/server/container_remove_test.go b/pkg/server/container_remove_test.go new file mode 100644 index 000000000..d1afd372a --- /dev/null +++ b/pkg/server/container_remove_test.go @@ -0,0 +1,85 @@ +/* + Copyright The containerd 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 + + http://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. +*/ + +package server + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + + containerstore "github.com/containerd/containerd/pkg/store/container" +) + +// TestSetContainerRemoving tests setContainerRemoving sets removing +// state correctly. +func TestSetContainerRemoving(t *testing.T) { + testID := "test-id" + for desc, test := range map[string]struct { + status containerstore.Status + expectErr bool + }{ + "should return error when container is in running state": { + status: containerstore.Status{ + CreatedAt: time.Now().UnixNano(), + StartedAt: time.Now().UnixNano(), + }, + expectErr: true, + }, + "should return error when container is in starting state": { + status: containerstore.Status{ + CreatedAt: time.Now().UnixNano(), + Starting: true, + }, + expectErr: true, + }, + "should return error when container is in removing state": { + status: containerstore.Status{ + CreatedAt: time.Now().UnixNano(), + StartedAt: time.Now().UnixNano(), + FinishedAt: time.Now().UnixNano(), + Removing: true, + }, + expectErr: true, + }, + "should not return error when container is not running and removing": { + status: containerstore.Status{ + CreatedAt: time.Now().UnixNano(), + StartedAt: time.Now().UnixNano(), + FinishedAt: time.Now().UnixNano(), + }, + expectErr: false, + }, + } { + t.Logf("TestCase %q", desc) + container, err := containerstore.NewContainer( + containerstore.Metadata{ID: testID}, + containerstore.WithFakeStatus(test.status), + ) + assert.NoError(t, err) + err = setContainerRemoving(container) + if test.expectErr { + assert.Error(t, err) + assert.Equal(t, test.status, container.Status.Get(), "metadata should not be updated") + } else { + assert.NoError(t, err) + assert.True(t, container.Status.Get().Removing, "removing should be set") + assert.NoError(t, resetContainerRemoving(container)) + assert.False(t, container.Status.Get().Removing, "removing should be reset") + } + } +} diff --git a/vendor/github.com/containerd/cri/pkg/server/container_start.go b/pkg/server/container_start.go similarity index 95% rename from vendor/github.com/containerd/cri/pkg/server/container_start.go rename to pkg/server/container_start.go index d00eb3d8e..79c7f761e 100644 --- a/vendor/github.com/containerd/cri/pkg/server/container_start.go +++ b/pkg/server/container_start.go @@ -31,11 +31,11 @@ import ( "golang.org/x/net/context" runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" - ctrdutil "github.com/containerd/cri/pkg/containerd/util" - cioutil "github.com/containerd/cri/pkg/ioutil" - cio "github.com/containerd/cri/pkg/server/io" - containerstore "github.com/containerd/cri/pkg/store/container" - sandboxstore "github.com/containerd/cri/pkg/store/sandbox" + ctrdutil "github.com/containerd/containerd/pkg/containerd/util" + cioutil "github.com/containerd/containerd/pkg/ioutil" + cio "github.com/containerd/containerd/pkg/server/io" + containerstore "github.com/containerd/containerd/pkg/store/container" + sandboxstore "github.com/containerd/containerd/pkg/store/sandbox" ) // StartContainer starts the container. @@ -126,7 +126,8 @@ func (c *criService) StartContainer(ctx context.Context, r *runtime.StartContain } if nric != nil { nriSB := &nri.Sandbox{ - ID: sandboxID, + ID: sandboxID, + Labels: sandbox.Config.Labels, } if _, err := nric.InvokeWithSandbox(ctx, task, v1.Create, nriSB); err != nil { return nil, errors.Wrap(err, "nri invoke") diff --git a/pkg/server/container_start_test.go b/pkg/server/container_start_test.go new file mode 100644 index 000000000..5ca308687 --- /dev/null +++ b/pkg/server/container_start_test.go @@ -0,0 +1,98 @@ +/* + Copyright The containerd 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 + + http://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. +*/ + +package server + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + + containerstore "github.com/containerd/containerd/pkg/store/container" +) + +// TestSetContainerStarting tests setContainerStarting sets removing +// state correctly. +func TestSetContainerStarting(t *testing.T) { + testID := "test-id" + for desc, test := range map[string]struct { + status containerstore.Status + expectErr bool + }{ + + "should not return error when container is in created state": { + status: containerstore.Status{ + CreatedAt: time.Now().UnixNano(), + }, + expectErr: false, + }, + "should return error when container is in running state": { + status: containerstore.Status{ + CreatedAt: time.Now().UnixNano(), + StartedAt: time.Now().UnixNano(), + }, + expectErr: true, + }, + "should return error when container is in exited state": { + status: containerstore.Status{ + CreatedAt: time.Now().UnixNano(), + StartedAt: time.Now().UnixNano(), + FinishedAt: time.Now().UnixNano(), + }, + expectErr: true, + }, + "should return error when container is in unknown state": { + status: containerstore.Status{ + CreatedAt: 0, + StartedAt: 0, + FinishedAt: 0, + }, + expectErr: true, + }, + "should return error when container is in starting state": { + status: containerstore.Status{ + CreatedAt: time.Now().UnixNano(), + Starting: true, + }, + expectErr: true, + }, + "should return error when container is in removing state": { + status: containerstore.Status{ + CreatedAt: time.Now().UnixNano(), + Removing: true, + }, + expectErr: true, + }, + } { + t.Logf("TestCase %q", desc) + container, err := containerstore.NewContainer( + containerstore.Metadata{ID: testID}, + containerstore.WithFakeStatus(test.status), + ) + assert.NoError(t, err) + err = setContainerStarting(container) + if test.expectErr { + assert.Error(t, err) + assert.Equal(t, test.status, container.Status.Get(), "metadata should not be updated") + } else { + assert.NoError(t, err) + assert.True(t, container.Status.Get().Starting, "starting should be set") + assert.NoError(t, resetContainerStarting(container)) + assert.False(t, container.Status.Get().Starting, "starting should be reset") + } + } +} diff --git a/vendor/github.com/containerd/cri/pkg/server/container_stats.go b/pkg/server/container_stats.go similarity index 100% rename from vendor/github.com/containerd/cri/pkg/server/container_stats.go rename to pkg/server/container_stats.go diff --git a/vendor/github.com/containerd/cri/pkg/server/container_stats_list.go b/pkg/server/container_stats_list.go similarity index 98% rename from vendor/github.com/containerd/cri/pkg/server/container_stats_list.go rename to pkg/server/container_stats_list.go index 0a9be8741..3b6ea29ac 100644 --- a/vendor/github.com/containerd/cri/pkg/server/container_stats_list.go +++ b/pkg/server/container_stats_list.go @@ -23,7 +23,7 @@ import ( "golang.org/x/net/context" runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" - containerstore "github.com/containerd/cri/pkg/store/container" + containerstore "github.com/containerd/containerd/pkg/store/container" ) // ListContainerStats returns stats of all running containers. diff --git a/vendor/github.com/containerd/cri/pkg/server/container_stats_list_unix.go b/pkg/server/container_stats_list_linux.go similarity index 97% rename from vendor/github.com/containerd/cri/pkg/server/container_stats_list_unix.go rename to pkg/server/container_stats_list_linux.go index ad398bc7a..82b040d3f 100644 --- a/vendor/github.com/containerd/cri/pkg/server/container_stats_list_unix.go +++ b/pkg/server/container_stats_list_linux.go @@ -1,5 +1,3 @@ -// +build !windows - /* Copyright The containerd Authors. @@ -28,7 +26,7 @@ import ( "github.com/pkg/errors" runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" - containerstore "github.com/containerd/cri/pkg/store/container" + containerstore "github.com/containerd/containerd/pkg/store/container" ) func (c *criService) containerMetrics( diff --git a/pkg/server/container_stats_list_linux_test.go b/pkg/server/container_stats_list_linux_test.go new file mode 100644 index 000000000..a35b5f21a --- /dev/null +++ b/pkg/server/container_stats_list_linux_test.go @@ -0,0 +1,55 @@ +/* + Copyright The containerd 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 + + http://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. +*/ + +package server + +import ( + "testing" + + v1 "github.com/containerd/cgroups/stats/v1" + "github.com/stretchr/testify/assert" +) + +func TestGetWorkingSet(t *testing.T) { + for desc, test := range map[string]struct { + memory *v1.MemoryStat + expected uint64 + }{ + "nil memory usage": { + memory: &v1.MemoryStat{}, + expected: 0, + }, + "memory usage higher than inactive_total_file": { + memory: &v1.MemoryStat{ + TotalInactiveFile: 1000, + Usage: &v1.MemoryEntry{Usage: 2000}, + }, + expected: 1000, + }, + "memory usage lower than inactive_total_file": { + memory: &v1.MemoryStat{ + TotalInactiveFile: 2000, + Usage: &v1.MemoryEntry{Usage: 1000}, + }, + expected: 0, + }, + } { + t.Run(desc, func(t *testing.T) { + got := getWorkingSet(test.memory) + assert.Equal(t, test.expected, got) + }) + } +} diff --git a/pkg/server/container_stats_list_other.go b/pkg/server/container_stats_list_other.go new file mode 100644 index 000000000..02a99978a --- /dev/null +++ b/pkg/server/container_stats_list_other.go @@ -0,0 +1,36 @@ +// +build !windows,!linux + +/* + Copyright The containerd 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 + + http://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. +*/ + +package server + +import ( + "github.com/containerd/containerd/api/types" + "github.com/containerd/containerd/errdefs" + "github.com/pkg/errors" + runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" + + containerstore "github.com/containerd/containerd/pkg/store/container" +) + +func (c *criService) containerMetrics( + meta containerstore.Metadata, + stats *types.Metric, +) (*runtime.ContainerStats, error) { + var cs runtime.ContainerStats + return &cs, errors.Wrap(errdefs.ErrNotImplemented, "container metrics") +} diff --git a/vendor/github.com/containerd/cri/pkg/server/container_stats_list_windows.go b/pkg/server/container_stats_list_windows.go similarity index 97% rename from vendor/github.com/containerd/cri/pkg/server/container_stats_list_windows.go rename to pkg/server/container_stats_list_windows.go index 4bd3b64c1..733a06d07 100644 --- a/vendor/github.com/containerd/cri/pkg/server/container_stats_list_windows.go +++ b/pkg/server/container_stats_list_windows.go @@ -25,7 +25,7 @@ import ( "github.com/pkg/errors" runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" - containerstore "github.com/containerd/cri/pkg/store/container" + containerstore "github.com/containerd/containerd/pkg/store/container" ) func (c *criService) containerMetrics( diff --git a/vendor/github.com/containerd/cri/pkg/server/container_status.go b/pkg/server/container_status.go similarity index 97% rename from vendor/github.com/containerd/cri/pkg/server/container_status.go rename to pkg/server/container_status.go index aeeb76db3..952660efa 100644 --- a/vendor/github.com/containerd/cri/pkg/server/container_status.go +++ b/pkg/server/container_status.go @@ -24,8 +24,8 @@ import ( "golang.org/x/net/context" runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" - "github.com/containerd/cri/pkg/store" - containerstore "github.com/containerd/cri/pkg/store/container" + "github.com/containerd/containerd/pkg/store" + containerstore "github.com/containerd/containerd/pkg/store/container" ) // ContainerStatus inspects the container and returns the status. diff --git a/pkg/server/container_status_test.go b/pkg/server/container_status_test.go new file mode 100644 index 000000000..da036aac7 --- /dev/null +++ b/pkg/server/container_status_test.go @@ -0,0 +1,227 @@ +/* + Copyright The containerd 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 + + http://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. +*/ + +package server + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "golang.org/x/net/context" + runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" + + containerstore "github.com/containerd/containerd/pkg/store/container" + imagestore "github.com/containerd/containerd/pkg/store/image" +) + +func getContainerStatusTestData() (*containerstore.Metadata, *containerstore.Status, + *imagestore.Image, *runtime.ContainerStatus) { + imageID := "sha256:1123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + testID := "test-id" + config := &runtime.ContainerConfig{ + Metadata: &runtime.ContainerMetadata{ + Name: "test-name", + Attempt: 1, + }, + Image: &runtime.ImageSpec{Image: "test-image"}, + Mounts: []*runtime.Mount{{ + ContainerPath: "test-container-path", + HostPath: "test-host-path", + }}, + Labels: map[string]string{"a": "b"}, + Annotations: map[string]string{"c": "d"}, + } + + createdAt := time.Now().UnixNano() + startedAt := time.Now().UnixNano() + + metadata := &containerstore.Metadata{ + ID: testID, + Name: "test-long-name", + SandboxID: "test-sandbox-id", + Config: config, + ImageRef: imageID, + LogPath: "test-log-path", + } + status := &containerstore.Status{ + Pid: 1234, + CreatedAt: createdAt, + StartedAt: startedAt, + } + image := &imagestore.Image{ + ID: imageID, + References: []string{ + "gcr.io/library/busybox:latest", + "gcr.io/library/busybox@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582", + }, + } + expected := &runtime.ContainerStatus{ + Id: testID, + Metadata: config.GetMetadata(), + State: runtime.ContainerState_CONTAINER_RUNNING, + CreatedAt: createdAt, + StartedAt: startedAt, + Image: &runtime.ImageSpec{Image: "gcr.io/library/busybox:latest"}, + ImageRef: "gcr.io/library/busybox@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582", + Reason: completeExitReason, + Labels: config.GetLabels(), + Annotations: config.GetAnnotations(), + Mounts: config.GetMounts(), + LogPath: "test-log-path", + } + + return metadata, status, image, expected +} + +func TestToCRIContainerStatus(t *testing.T) { + for desc, test := range map[string]struct { + finishedAt int64 + exitCode int32 + reason string + message string + expectedState runtime.ContainerState + expectedReason string + }{ + "container running": { + expectedState: runtime.ContainerState_CONTAINER_RUNNING, + }, + "container exited with reason": { + finishedAt: time.Now().UnixNano(), + exitCode: 1, + reason: "test-reason", + message: "test-message", + expectedState: runtime.ContainerState_CONTAINER_EXITED, + expectedReason: "test-reason", + }, + "container exited with exit code 0 without reason": { + finishedAt: time.Now().UnixNano(), + exitCode: 0, + message: "test-message", + expectedState: runtime.ContainerState_CONTAINER_EXITED, + expectedReason: completeExitReason, + }, + "container exited with non-zero exit code without reason": { + finishedAt: time.Now().UnixNano(), + exitCode: 1, + message: "test-message", + expectedState: runtime.ContainerState_CONTAINER_EXITED, + expectedReason: errorExitReason, + }, + } { + metadata, status, _, expected := getContainerStatusTestData() + // Update status with test case. + status.FinishedAt = test.finishedAt + status.ExitCode = test.exitCode + status.Reason = test.reason + status.Message = test.message + container, err := containerstore.NewContainer( + *metadata, + containerstore.WithFakeStatus(*status), + ) + assert.NoError(t, err) + // Set expectation based on test case. + expected.State = test.expectedState + expected.Reason = test.expectedReason + expected.FinishedAt = test.finishedAt + expected.ExitCode = test.exitCode + expected.Message = test.message + containerStatus := toCRIContainerStatus(container, + expected.Image, + expected.ImageRef) + assert.Equal(t, expected, containerStatus, desc) + } +} + +// TODO(mikebrow): add a fake containerd container.Container.Spec client api so we can test verbose is true option +func TestToCRIContainerInfo(t *testing.T) { + metadata, status, _, _ := getContainerStatusTestData() + container, err := containerstore.NewContainer( + *metadata, + containerstore.WithFakeStatus(*status), + ) + assert.NoError(t, err) + + info, err := toCRIContainerInfo(context.Background(), + container, + false) + assert.NoError(t, err) + assert.Nil(t, info) +} + +func TestContainerStatus(t *testing.T) { + for desc, test := range map[string]struct { + exist bool + imageExist bool + finishedAt int64 + reason string + expectedState runtime.ContainerState + expectErr bool + }{ + "container running": { + exist: true, + imageExist: true, + expectedState: runtime.ContainerState_CONTAINER_RUNNING, + }, + "container exited": { + exist: true, + imageExist: true, + finishedAt: time.Now().UnixNano(), + reason: "test-reason", + expectedState: runtime.ContainerState_CONTAINER_EXITED, + }, + "container not exist": { + exist: false, + imageExist: true, + expectErr: true, + }, + "image not exist": { + exist: false, + imageExist: false, + expectErr: true, + }, + } { + t.Logf("TestCase %q", desc) + c := newTestCRIService() + metadata, status, image, expected := getContainerStatusTestData() + // Update status with test case. + status.FinishedAt = test.finishedAt + status.Reason = test.reason + container, err := containerstore.NewContainer( + *metadata, + containerstore.WithFakeStatus(*status), + ) + assert.NoError(t, err) + if test.exist { + assert.NoError(t, c.containerStore.Add(container)) + } + if test.imageExist { + c.imageStore, err = imagestore.NewFakeStore([]imagestore.Image{*image}) + assert.NoError(t, err) + } + resp, err := c.ContainerStatus(context.Background(), &runtime.ContainerStatusRequest{ContainerId: container.ID}) + if test.expectErr { + assert.Error(t, err) + assert.Nil(t, resp) + continue + } + // Set expectation based on test case. + expected.FinishedAt = test.finishedAt + expected.Reason = test.reason + expected.State = test.expectedState + assert.Equal(t, expected, resp.GetStatus()) + } +} diff --git a/vendor/github.com/containerd/cri/pkg/server/container_stop.go b/pkg/server/container_stop.go similarity index 97% rename from vendor/github.com/containerd/cri/pkg/server/container_stop.go rename to pkg/server/container_stop.go index 92075d6b6..469376e45 100644 --- a/vendor/github.com/containerd/cri/pkg/server/container_stop.go +++ b/pkg/server/container_stop.go @@ -28,9 +28,9 @@ import ( "golang.org/x/net/context" runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" - ctrdutil "github.com/containerd/cri/pkg/containerd/util" - "github.com/containerd/cri/pkg/store" - containerstore "github.com/containerd/cri/pkg/store/container" + ctrdutil "github.com/containerd/containerd/pkg/containerd/util" + "github.com/containerd/containerd/pkg/store" + containerstore "github.com/containerd/containerd/pkg/store/container" ) // StopContainer stops a running container with a grace period (i.e., timeout). diff --git a/pkg/server/container_stop_test.go b/pkg/server/container_stop_test.go new file mode 100644 index 000000000..8d2fa140a --- /dev/null +++ b/pkg/server/container_stop_test.go @@ -0,0 +1,85 @@ +/* + Copyright The containerd 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 + + http://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. +*/ + +package server + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "golang.org/x/net/context" + + containerstore "github.com/containerd/containerd/pkg/store/container" +) + +func TestWaitContainerStop(t *testing.T) { + id := "test-id" + for desc, test := range map[string]struct { + status *containerstore.Status + cancel bool + timeout time.Duration + expectErr bool + }{ + "should return error if timeout exceeds": { + status: &containerstore.Status{ + CreatedAt: time.Now().UnixNano(), + StartedAt: time.Now().UnixNano(), + }, + timeout: 200 * time.Millisecond, + expectErr: true, + }, + "should return error if context is cancelled": { + status: &containerstore.Status{ + CreatedAt: time.Now().UnixNano(), + StartedAt: time.Now().UnixNano(), + }, + timeout: time.Hour, + cancel: true, + expectErr: true, + }, + "should not return error if container is stopped before timeout": { + status: &containerstore.Status{ + CreatedAt: time.Now().UnixNano(), + StartedAt: time.Now().UnixNano(), + FinishedAt: time.Now().UnixNano(), + }, + timeout: time.Hour, + expectErr: false, + }, + } { + c := newTestCRIService() + container, err := containerstore.NewContainer( + containerstore.Metadata{ID: id}, + containerstore.WithFakeStatus(*test.status), + ) + assert.NoError(t, err) + assert.NoError(t, c.containerStore.Add(container)) + ctx := context.Background() + if test.cancel { + cancelledCtx, cancel := context.WithCancel(ctx) + cancel() + ctx = cancelledCtx + } + if test.timeout > 0 { + timeoutCtx, cancel := context.WithTimeout(ctx, test.timeout) + defer cancel() + ctx = timeoutCtx + } + err = c.waitContainerStop(ctx, container) + assert.Equal(t, test.expectErr, err != nil, desc) + } +} diff --git a/vendor/github.com/containerd/cri/pkg/server/container_update_resources_unix.go b/pkg/server/container_update_resources_linux.go similarity index 95% rename from vendor/github.com/containerd/cri/pkg/server/container_update_resources_unix.go rename to pkg/server/container_update_resources_linux.go index 23e0d409b..6857575bd 100644 --- a/vendor/github.com/containerd/cri/pkg/server/container_update_resources_unix.go +++ b/pkg/server/container_update_resources_linux.go @@ -1,5 +1,3 @@ -// +build !windows - /* Copyright The containerd Authors. @@ -31,10 +29,10 @@ import ( "golang.org/x/net/context" runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" - "github.com/containerd/cri/pkg/containerd/opts" - ctrdutil "github.com/containerd/cri/pkg/containerd/util" - containerstore "github.com/containerd/cri/pkg/store/container" - "github.com/containerd/cri/pkg/util" + "github.com/containerd/containerd/pkg/containerd/opts" + ctrdutil "github.com/containerd/containerd/pkg/containerd/util" + containerstore "github.com/containerd/containerd/pkg/store/container" + "github.com/containerd/containerd/pkg/util" ) // UpdateContainerResources updates ContainerConfig of the container. diff --git a/pkg/server/container_update_resources_linux_test.go b/pkg/server/container_update_resources_linux_test.go new file mode 100644 index 000000000..ffbc3f88f --- /dev/null +++ b/pkg/server/container_update_resources_linux_test.go @@ -0,0 +1,162 @@ +/* + Copyright The containerd 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 + + http://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. +*/ + +package server + +import ( + "context" + "testing" + + "github.com/golang/protobuf/proto" + runtimespec "github.com/opencontainers/runtime-spec/specs-go" + "github.com/stretchr/testify/assert" + runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" +) + +func TestUpdateOCILinuxResource(t *testing.T) { + oomscoreadj := new(int) + *oomscoreadj = -500 + for desc, test := range map[string]struct { + spec *runtimespec.Spec + resources *runtime.LinuxContainerResources + expected *runtimespec.Spec + expectErr bool + }{ + "should be able to update each resource": { + spec: &runtimespec.Spec{ + Process: &runtimespec.Process{OOMScoreAdj: oomscoreadj}, + Linux: &runtimespec.Linux{ + Resources: &runtimespec.LinuxResources{ + Memory: &runtimespec.LinuxMemory{Limit: proto.Int64(12345)}, + CPU: &runtimespec.LinuxCPU{ + Shares: proto.Uint64(1111), + Quota: proto.Int64(2222), + Period: proto.Uint64(3333), + Cpus: "0-1", + Mems: "2-3", + }, + }, + }, + }, + resources: &runtime.LinuxContainerResources{ + CpuPeriod: 6666, + CpuQuota: 5555, + CpuShares: 4444, + MemoryLimitInBytes: 54321, + OomScoreAdj: 500, + CpusetCpus: "4-5", + CpusetMems: "6-7", + }, + expected: &runtimespec.Spec{ + Process: &runtimespec.Process{OOMScoreAdj: oomscoreadj}, + Linux: &runtimespec.Linux{ + Resources: &runtimespec.LinuxResources{ + Memory: &runtimespec.LinuxMemory{Limit: proto.Int64(54321)}, + CPU: &runtimespec.LinuxCPU{ + Shares: proto.Uint64(4444), + Quota: proto.Int64(5555), + Period: proto.Uint64(6666), + Cpus: "4-5", + Mems: "6-7", + }, + }, + }, + }, + }, + "should skip empty fields": { + spec: &runtimespec.Spec{ + Process: &runtimespec.Process{OOMScoreAdj: oomscoreadj}, + Linux: &runtimespec.Linux{ + Resources: &runtimespec.LinuxResources{ + Memory: &runtimespec.LinuxMemory{Limit: proto.Int64(12345)}, + CPU: &runtimespec.LinuxCPU{ + Shares: proto.Uint64(1111), + Quota: proto.Int64(2222), + Period: proto.Uint64(3333), + Cpus: "0-1", + Mems: "2-3", + }, + }, + }, + }, + resources: &runtime.LinuxContainerResources{ + CpuQuota: 5555, + CpuShares: 4444, + MemoryLimitInBytes: 54321, + OomScoreAdj: 500, + CpusetMems: "6-7", + }, + expected: &runtimespec.Spec{ + Process: &runtimespec.Process{OOMScoreAdj: oomscoreadj}, + Linux: &runtimespec.Linux{ + Resources: &runtimespec.LinuxResources{ + Memory: &runtimespec.LinuxMemory{Limit: proto.Int64(54321)}, + CPU: &runtimespec.LinuxCPU{ + Shares: proto.Uint64(4444), + Quota: proto.Int64(5555), + Period: proto.Uint64(3333), + Cpus: "0-1", + Mems: "6-7", + }, + }, + }, + }, + }, + "should be able to fill empty fields": { + spec: &runtimespec.Spec{ + Process: &runtimespec.Process{OOMScoreAdj: oomscoreadj}, + Linux: &runtimespec.Linux{ + Resources: &runtimespec.LinuxResources{ + Memory: &runtimespec.LinuxMemory{Limit: proto.Int64(12345)}, + }, + }, + }, + resources: &runtime.LinuxContainerResources{ + CpuPeriod: 6666, + CpuQuota: 5555, + CpuShares: 4444, + MemoryLimitInBytes: 54321, + OomScoreAdj: 500, + CpusetCpus: "4-5", + CpusetMems: "6-7", + }, + expected: &runtimespec.Spec{ + Process: &runtimespec.Process{OOMScoreAdj: oomscoreadj}, + Linux: &runtimespec.Linux{ + Resources: &runtimespec.LinuxResources{ + Memory: &runtimespec.LinuxMemory{Limit: proto.Int64(54321)}, + CPU: &runtimespec.LinuxCPU{ + Shares: proto.Uint64(4444), + Quota: proto.Int64(5555), + Period: proto.Uint64(6666), + Cpus: "4-5", + Mems: "6-7", + }, + }, + }, + }, + }, + } { + t.Logf("TestCase %q", desc) + got, err := updateOCILinuxResource(context.Background(), test.spec, test.resources, false, false) + if test.expectErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + assert.Equal(t, test.expected, got) + } +} diff --git a/pkg/server/container_update_resources_other.go b/pkg/server/container_update_resources_other.go new file mode 100644 index 000000000..e4370324b --- /dev/null +++ b/pkg/server/container_update_resources_other.go @@ -0,0 +1,44 @@ +// +build !windows,!linux + +/* + Copyright The containerd 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 + + http://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. +*/ + +package server + +import ( + "github.com/pkg/errors" + "golang.org/x/net/context" + runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" + + containerstore "github.com/containerd/containerd/pkg/store/container" +) + +// UpdateContainerResources updates ContainerConfig of the container. +func (c *criService) UpdateContainerResources(ctx context.Context, r *runtime.UpdateContainerResourcesRequest) (retRes *runtime.UpdateContainerResourcesResponse, retErr error) { + container, err := c.containerStore.Get(r.GetContainerId()) + if err != nil { + return nil, errors.Wrap(err, "failed to find container") + } + // Update resources in status update transaction, so that: + // 1) There won't be race condition with container start. + // 2) There won't be concurrent resource update to the same container. + if err := container.Status.Update(func(status containerstore.Status) (containerstore.Status, error) { + return status, nil + }); err != nil { + return nil, errors.Wrap(err, "failed to update resources") + } + return &runtime.UpdateContainerResourcesResponse{}, nil +} diff --git a/vendor/github.com/containerd/cri/pkg/server/container_update_resources_windows.go b/pkg/server/container_update_resources_windows.go similarity index 100% rename from vendor/github.com/containerd/cri/pkg/server/container_update_resources_windows.go rename to pkg/server/container_update_resources_windows.go diff --git a/vendor/github.com/containerd/cri/pkg/server/events.go b/pkg/server/events.go similarity index 97% rename from vendor/github.com/containerd/cri/pkg/server/events.go rename to pkg/server/events.go index 8f35bf0bd..b9aabe597 100644 --- a/vendor/github.com/containerd/cri/pkg/server/events.go +++ b/pkg/server/events.go @@ -32,11 +32,11 @@ import ( "golang.org/x/net/context" "k8s.io/apimachinery/pkg/util/clock" - "github.com/containerd/cri/pkg/constants" - ctrdutil "github.com/containerd/cri/pkg/containerd/util" - "github.com/containerd/cri/pkg/store" - containerstore "github.com/containerd/cri/pkg/store/container" - sandboxstore "github.com/containerd/cri/pkg/store/sandbox" + "github.com/containerd/containerd/pkg/constants" + ctrdutil "github.com/containerd/containerd/pkg/containerd/util" + "github.com/containerd/containerd/pkg/store" + containerstore "github.com/containerd/containerd/pkg/store/container" + sandboxstore "github.com/containerd/containerd/pkg/store/sandbox" ) const ( diff --git a/pkg/server/events_test.go b/pkg/server/events_test.go new file mode 100644 index 000000000..c7b49aa51 --- /dev/null +++ b/pkg/server/events_test.go @@ -0,0 +1,134 @@ +/* + Copyright The containerd 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 + + http://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. +*/ + +package server + +import ( + "testing" + "time" + + eventtypes "github.com/containerd/containerd/api/events" + "github.com/containerd/typeurl" + "github.com/stretchr/testify/assert" + "k8s.io/apimachinery/pkg/util/clock" +) + +// TestBackOff tests the logic of backOff struct. +func TestBackOff(t *testing.T) { + testStartTime := time.Now() + testClock := clock.NewFakeClock(testStartTime) + inputQueues := map[string]*backOffQueue{ + "container1": { + events: []interface{}{ + &eventtypes.TaskOOM{ContainerID: "container1"}, + &eventtypes.TaskOOM{ContainerID: "container1"}, + }, + }, + "container2": { + events: []interface{}{ + &eventtypes.TaskOOM{ContainerID: "container2"}, + &eventtypes.TaskOOM{ContainerID: "container2"}, + }, + }, + } + expectedQueues := map[string]*backOffQueue{ + "container2": { + events: []interface{}{ + &eventtypes.TaskOOM{ContainerID: "container2"}, + &eventtypes.TaskOOM{ContainerID: "container2"}, + }, + expireTime: testClock.Now().Add(backOffInitDuration), + duration: backOffInitDuration, + clock: testClock, + }, + "container1": { + events: []interface{}{ + &eventtypes.TaskOOM{ContainerID: "container1"}, + &eventtypes.TaskOOM{ContainerID: "container1"}, + }, + expireTime: testClock.Now().Add(backOffInitDuration), + duration: backOffInitDuration, + clock: testClock, + }, + } + + t.Logf("Should be able to backOff a event") + actual := newBackOff() + actual.clock = testClock + for k, queue := range inputQueues { + for _, event := range queue.events { + actual.enBackOff(k, event) + } + } + assert.Equal(t, actual.queuePool, expectedQueues) + + t.Logf("Should be able to check if the container is in backOff state") + for k, queue := range inputQueues { + for _, e := range queue.events { + any, err := typeurl.MarshalAny(e) + assert.NoError(t, err) + key, _, err := convertEvent(any) + assert.NoError(t, err) + assert.Equal(t, k, key) + assert.Equal(t, actual.isInBackOff(key), true) + } + } + + t.Logf("Should be able to check that a container isn't in backOff state") + notExistKey := "containerNotExist" + assert.Equal(t, actual.isInBackOff(notExistKey), false) + + t.Logf("No containers should be expired") + assert.Empty(t, actual.getExpiredIDs()) + + t.Logf("Should be able to get all keys which are expired for backOff") + testClock.Sleep(backOffInitDuration) + actKeyList := actual.getExpiredIDs() + assert.Equal(t, len(inputQueues), len(actKeyList)) + for k := range inputQueues { + assert.Contains(t, actKeyList, k) + } + + t.Logf("Should be able to get out all backOff events") + doneQueues := map[string]*backOffQueue{} + for k := range inputQueues { + actQueue := actual.deBackOff(k) + doneQueues[k] = actQueue + assert.Equal(t, actQueue, expectedQueues[k]) + } + + t.Logf("Should not get out the event again after having got out the backOff event") + for k := range inputQueues { + var expect *backOffQueue + actQueue := actual.deBackOff(k) + assert.Equal(t, actQueue, expect) + } + + t.Logf("Should be able to reBackOff") + for k, queue := range doneQueues { + failEventIndex := 1 + events := queue.events[failEventIndex:] + actual.reBackOff(k, events, queue.duration) + actQueue := actual.deBackOff(k) + expQueue := &backOffQueue{ + events: events, + expireTime: testClock.Now().Add(2 * queue.duration), + duration: 2 * queue.duration, + clock: testClock, + } + assert.Equal(t, actQueue, expQueue) + } +} diff --git a/vendor/github.com/containerd/cri/pkg/server/helpers.go b/pkg/server/helpers.go similarity index 97% rename from vendor/github.com/containerd/cri/pkg/server/helpers.go rename to pkg/server/helpers.go index 34da9a254..0e09d0321 100644 --- a/vendor/github.com/containerd/cri/pkg/server/helpers.go +++ b/pkg/server/helpers.go @@ -37,12 +37,12 @@ import ( "golang.org/x/net/context" runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" - runtimeoptions "github.com/containerd/cri/pkg/api/runtimeoptions/v1" - criconfig "github.com/containerd/cri/pkg/config" - "github.com/containerd/cri/pkg/store" - containerstore "github.com/containerd/cri/pkg/store/container" - imagestore "github.com/containerd/cri/pkg/store/image" - sandboxstore "github.com/containerd/cri/pkg/store/sandbox" + runtimeoptions "github.com/containerd/containerd/pkg/api/runtimeoptions/v1" + criconfig "github.com/containerd/containerd/pkg/config" + "github.com/containerd/containerd/pkg/store" + containerstore "github.com/containerd/containerd/pkg/store/container" + imagestore "github.com/containerd/containerd/pkg/store/image" + sandboxstore "github.com/containerd/containerd/pkg/store/sandbox" ) const ( diff --git a/vendor/github.com/containerd/cri/pkg/server/helpers_unix.go b/pkg/server/helpers_linux.go similarity index 98% rename from vendor/github.com/containerd/cri/pkg/server/helpers_unix.go rename to pkg/server/helpers_linux.go index 0c3a6652d..6fc70ede8 100644 --- a/vendor/github.com/containerd/cri/pkg/server/helpers_unix.go +++ b/pkg/server/helpers_linux.go @@ -1,5 +1,3 @@ -// +build !windows - /* Copyright The containerd Authors. @@ -32,8 +30,8 @@ import ( "github.com/containerd/containerd/log" "github.com/containerd/containerd/mount" - "github.com/containerd/cri/pkg/seccomp" - "github.com/containerd/cri/pkg/seutil" + "github.com/containerd/containerd/pkg/seccomp" + "github.com/containerd/containerd/pkg/seutil" runcapparmor "github.com/opencontainers/runc/libcontainer/apparmor" "github.com/opencontainers/runtime-spec/specs-go" "github.com/opencontainers/selinux/go-selinux/label" diff --git a/pkg/server/helpers_linux_test.go b/pkg/server/helpers_linux_test.go new file mode 100644 index 000000000..ca19c154a --- /dev/null +++ b/pkg/server/helpers_linux_test.go @@ -0,0 +1,106 @@ +/* + Copyright The containerd 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 + + http://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. +*/ + +package server + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "golang.org/x/net/context" + "golang.org/x/sys/unix" +) + +func TestGetCgroupsPath(t *testing.T) { + testID := "test-id" + for desc, test := range map[string]struct { + cgroupsParent string + expected string + }{ + "should support regular cgroup path": { + cgroupsParent: "/a/b", + expected: "/a/b/test-id", + }, + "should support systemd cgroup path": { + cgroupsParent: "/a.slice/b.slice", + expected: "b.slice:cri-containerd:test-id", + }, + "should support tailing slash for regular cgroup path": { + cgroupsParent: "/a/b/", + expected: "/a/b/test-id", + }, + "should support tailing slash for systemd cgroup path": { + cgroupsParent: "/a.slice/b.slice/", + expected: "b.slice:cri-containerd:test-id", + }, + "should treat root cgroup as regular cgroup path": { + cgroupsParent: "/", + expected: "/test-id", + }, + } { + t.Logf("TestCase %q", desc) + got := getCgroupsPath(test.cgroupsParent, testID) + assert.Equal(t, test.expected, got) + } +} + +func TestEnsureRemoveAllWithMount(t *testing.T) { + if os.Getuid() != 0 { + t.Skip("skipping test that requires root") + } + + dir1, err := ioutil.TempDir("", "test-ensure-removeall-with-dir1") + if err != nil { + t.Fatal(err) + } + dir2, err := ioutil.TempDir("", "test-ensure-removeall-with-dir2") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(dir2) + + bindDir := filepath.Join(dir1, "bind") + if err := os.MkdirAll(bindDir, 0755); err != nil { + t.Fatal(err) + } + + if err := unix.Mount(dir2, bindDir, "none", unix.MS_BIND, ""); err != nil { + t.Fatal(err) + } + + done := make(chan struct{}) + go func() { + err = ensureRemoveAll(context.Background(), dir1) + close(done) + }() + + select { + case <-done: + if err != nil { + t.Fatal(err) + } + case <-time.After(5 * time.Second): + t.Fatal("timeout waiting for EnsureRemoveAll to finish") + } + + if _, err := os.Stat(dir1); !os.IsNotExist(err) { + t.Fatalf("expected %q to not exist", dir1) + } +} diff --git a/pkg/server/helpers_other.go b/pkg/server/helpers_other.go new file mode 100644 index 000000000..6a67375d7 --- /dev/null +++ b/pkg/server/helpers_other.go @@ -0,0 +1,43 @@ +// +build !windows,!linux + +/* + Copyright The containerd 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 + + http://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. +*/ + +package server + +import ( + "context" + "os" + + "github.com/opencontainers/runtime-spec/specs-go" +) + +// openLogFile opens/creates a container log file. +func openLogFile(path string) (*os.File, error) { + return os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0640) +} + +// ensureRemoveAll wraps `os.RemoveAll` to check for specific errors that can +// often be remedied. +// Only use `ensureRemoveAll` if you really want to make every effort to remove +// a directory. +func ensureRemoveAll(ctx context.Context, dir string) error { + return os.RemoveAll(dir) +} + +func modifyProcessLabel(runtimeType string, spec *specs.Spec) error { + return nil +} diff --git a/pkg/server/helpers_selinux_test.go b/pkg/server/helpers_selinux_test.go new file mode 100644 index 000000000..53ed59c5a --- /dev/null +++ b/pkg/server/helpers_selinux_test.go @@ -0,0 +1,159 @@ +// +build selinux + +/* + Copyright The containerd 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 + + http://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. +*/ + +package server + +import ( + "testing" + + "github.com/opencontainers/selinux/go-selinux" + "github.com/stretchr/testify/assert" + runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" +) + +func TestInitSelinuxOpts(t *testing.T) { + if !selinux.GetEnabled() { + t.Skip("selinux is not enabled") + } + + for desc, test := range map[string]struct { + selinuxOpt *runtime.SELinuxOption + processLabel string + mountLabel string + expectErr bool + }{ + "Should return empty strings for processLabel and mountLabel when selinuxOpt is nil": { + selinuxOpt: nil, + processLabel: ".*:c[0-9]{1,3},c[0-9]{1,3}", + mountLabel: ".*:c[0-9]{1,3},c[0-9]{1,3}", + }, + "Should overlay fields on processLabel when selinuxOpt has been initialized partially": { + selinuxOpt: &runtime.SELinuxOption{ + User: "", + Role: "user_r", + Type: "", + Level: "s0:c1,c2", + }, + processLabel: "system_u:user_r:(container_file_t|svirt_lxc_net_t):s0:c1,c2", + mountLabel: "system_u:object_r:(container_file_t|svirt_sandbox_file_t):s0:c1,c2", + }, + "Should be resolved correctly when selinuxOpt has been initialized completely": { + selinuxOpt: &runtime.SELinuxOption{ + User: "user_u", + Role: "user_r", + Type: "user_t", + Level: "s0:c1,c2", + }, + processLabel: "user_u:user_r:user_t:s0:c1,c2", + mountLabel: "user_u:object_r:(container_file_t|svirt_sandbox_file_t):s0:c1,c2", + }, + "Should be resolved correctly when selinuxOpt has been initialized with level=''": { + selinuxOpt: &runtime.SELinuxOption{ + User: "user_u", + Role: "user_r", + Type: "user_t", + Level: "", + }, + processLabel: "user_u:user_r:user_t:s0:c[0-9]{1,3},c[0-9]{1,3}", + mountLabel: "user_u:object_r:(container_file_t|svirt_sandbox_file_t):s0", + }, + "Should return error when the format of 'level' is not correct": { + selinuxOpt: &runtime.SELinuxOption{ + User: "user_u", + Role: "user_r", + Type: "user_t", + Level: "s0,c1,c2", + }, + expectErr: true, + }, + } { + t.Run(desc, func(t *testing.T) { + processLabel, mountLabel, err := initLabelsFromOpt(test.selinuxOpt) + if test.expectErr { + assert.Error(t, err) + } else { + assert.Regexp(t, test.processLabel, processLabel) + assert.Regexp(t, test.mountLabel, mountLabel) + } + }) + } +} + +func TestCheckSelinuxLevel(t *testing.T) { + for desc, test := range map[string]struct { + level string + expectNoMatch bool + }{ + "s0": { + level: "s0", + }, + "s0-s0": { + level: "s0-s0", + }, + "s0:c0": { + level: "s0:c0", + }, + "s0:c0.c3": { + level: "s0:c0.c3", + }, + "s0:c0,c3": { + level: "s0:c0,c3", + }, + "s0-s0:c0,c3": { + level: "s0-s0:c0,c3", + }, + "s0-s0:c0,c3.c6": { + level: "s0-s0:c0,c3.c6", + }, + "s0-s0:c0,c3.c6,c8.c10": { + level: "s0-s0:c0,c3.c6,c8.c10", + }, + "s0-s0:c0,c3.c6,c8,c10": { + level: "s0-s0:c0,c3.c6", + }, + "s0,c0,c3": { + level: "s0,c0,c3", + expectNoMatch: true, + }, + "s0:c0.c3.c6": { + level: "s0:c0.c3.c6", + expectNoMatch: true, + }, + "s0-s0,c0,c3": { + level: "s0-s0,c0,c3", + expectNoMatch: true, + }, + "s0-s0:c0.c3.c6": { + level: "s0-s0:c0.c3.c6", + expectNoMatch: true, + }, + "s0-s0:c0,c3.c6.c8": { + level: "s0-s0:c0,c3.c6.c8", + expectNoMatch: true, + }, + } { + t.Run(desc, func(t *testing.T) { + err := checkSelinuxLevel(test.level) + if test.expectNoMatch { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/pkg/server/helpers_test.go b/pkg/server/helpers_test.go new file mode 100644 index 000000000..fa65002b2 --- /dev/null +++ b/pkg/server/helpers_test.go @@ -0,0 +1,498 @@ +/* + Copyright The containerd 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 + + http://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. +*/ + +package server + +import ( + "context" + "io/ioutil" + "testing" + + "github.com/BurntSushi/toml" + "github.com/containerd/containerd/oci" + "github.com/containerd/containerd/plugin" + "github.com/containerd/containerd/reference/docker" + "github.com/containerd/containerd/runtime/linux/runctypes" + runcoptions "github.com/containerd/containerd/runtime/v2/runc/options" + imagedigest "github.com/opencontainers/go-digest" + runtimespec "github.com/opencontainers/runtime-spec/specs-go" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + criconfig "github.com/containerd/containerd/pkg/config" + "github.com/containerd/containerd/pkg/store" + imagestore "github.com/containerd/containerd/pkg/store/image" +) + +// TestGetUserFromImage tests the logic of getting image uid or user name of image user. +func TestGetUserFromImage(t *testing.T) { + newI64 := func(i int64) *int64 { return &i } + for c, test := range map[string]struct { + user string + uid *int64 + name string + }{ + "no gid": { + user: "0", + uid: newI64(0), + }, + "uid/gid": { + user: "0:1", + uid: newI64(0), + }, + "empty user": { + user: "", + }, + "multiple spearators": { + user: "1:2:3", + uid: newI64(1), + }, + "root username": { + user: "root:root", + name: "root", + }, + "username": { + user: "test:test", + name: "test", + }, + } { + t.Logf("TestCase - %q", c) + actualUID, actualName := getUserFromImage(test.user) + assert.Equal(t, test.uid, actualUID) + assert.Equal(t, test.name, actualName) + } +} + +func TestGetRepoDigestAndTag(t *testing.T) { + digest := imagedigest.Digest("sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582") + for desc, test := range map[string]struct { + ref string + schema1 bool + expectedRepoDigest string + expectedRepoTag string + }{ + "repo tag should be empty if original ref has no tag": { + ref: "gcr.io/library/busybox@" + digest.String(), + expectedRepoDigest: "gcr.io/library/busybox@" + digest.String(), + }, + "repo tag should not be empty if original ref has tag": { + ref: "gcr.io/library/busybox:latest", + expectedRepoDigest: "gcr.io/library/busybox@" + digest.String(), + expectedRepoTag: "gcr.io/library/busybox:latest", + }, + "repo digest should be empty if original ref is schema1 and has no digest": { + ref: "gcr.io/library/busybox:latest", + schema1: true, + expectedRepoDigest: "", + expectedRepoTag: "gcr.io/library/busybox:latest", + }, + "repo digest should not be empty if orignal ref is schema1 but has digest": { + ref: "gcr.io/library/busybox@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59594", + schema1: true, + expectedRepoDigest: "gcr.io/library/busybox@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59594", + expectedRepoTag: "", + }, + } { + t.Logf("TestCase %q", desc) + named, err := docker.ParseDockerRef(test.ref) + assert.NoError(t, err) + repoDigest, repoTag := getRepoDigestAndTag(named, digest, test.schema1) + assert.Equal(t, test.expectedRepoDigest, repoDigest) + assert.Equal(t, test.expectedRepoTag, repoTag) + } +} + +func TestBuildLabels(t *testing.T) { + configLabels := map[string]string{ + "a": "b", + "c": "d", + } + newLabels := buildLabels(configLabels, containerKindSandbox) + assert.Len(t, newLabels, 3) + assert.Equal(t, "b", newLabels["a"]) + assert.Equal(t, "d", newLabels["c"]) + assert.Equal(t, containerKindSandbox, newLabels[containerKindLabel]) + + newLabels["a"] = "e" + assert.Empty(t, configLabels[containerKindLabel], "should not add new labels into original label") + assert.Equal(t, "b", configLabels["a"], "change in new labels should not affect original label") +} + +func TestParseImageReferences(t *testing.T) { + refs := []string{ + "gcr.io/library/busybox@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582", + "gcr.io/library/busybox:1.2", + "sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582", + "arbitrary-ref", + } + expectedTags := []string{ + "gcr.io/library/busybox:1.2", + } + expectedDigests := []string{"gcr.io/library/busybox@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582"} + tags, digests := parseImageReferences(refs) + assert.Equal(t, expectedTags, tags) + assert.Equal(t, expectedDigests, digests) +} + +func TestLocalResolve(t *testing.T) { + image := imagestore.Image{ + ID: "sha256:c75bebcdd211f41b3a460c7bf82970ed6c75acaab9cd4c9a4e125b03ca113799", + ChainID: "test-chain-id-1", + References: []string{ + "docker.io/library/busybox:latest", + "docker.io/library/busybox@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582", + }, + Size: 10, + } + c := newTestCRIService() + var err error + c.imageStore, err = imagestore.NewFakeStore([]imagestore.Image{image}) + assert.NoError(t, err) + + for _, ref := range []string{ + "sha256:c75bebcdd211f41b3a460c7bf82970ed6c75acaab9cd4c9a4e125b03ca113799", + "busybox", + "busybox:latest", + "busybox@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582", + "library/busybox", + "library/busybox:latest", + "library/busybox@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582", + "docker.io/busybox", + "docker.io/busybox:latest", + "docker.io/busybox@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582", + "docker.io/library/busybox", + "docker.io/library/busybox:latest", + "docker.io/library/busybox@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582", + } { + img, err := c.localResolve(ref) + assert.NoError(t, err) + assert.Equal(t, image, img) + } + img, err := c.localResolve("randomid") + assert.Equal(t, store.ErrNotExist, err) + assert.Equal(t, imagestore.Image{}, img) +} + +func TestGenerateRuntimeOptions(t *testing.T) { + nilOpts := ` +systemd_cgroup = true +[containerd] + no_pivot = true + default_runtime_name = "default" +[containerd.runtimes.legacy] + runtime_type = "` + plugin.RuntimeLinuxV1 + `" +[containerd.runtimes.runc] + runtime_type = "` + plugin.RuntimeRuncV1 + `" +[containerd.runtimes.runcv2] + runtime_type = "` + plugin.RuntimeRuncV2 + `" +` + nonNilOpts := ` +systemd_cgroup = true +[containerd] + no_pivot = true + default_runtime_name = "default" +[containerd.runtimes.legacy] + runtime_type = "` + plugin.RuntimeLinuxV1 + `" +[containerd.runtimes.legacy.options] + Runtime = "legacy" + RuntimeRoot = "/legacy" +[containerd.runtimes.runc] + runtime_type = "` + plugin.RuntimeRuncV1 + `" +[containerd.runtimes.runc.options] + BinaryName = "runc" + Root = "/runc" + NoNewKeyring = true +[containerd.runtimes.runcv2] + runtime_type = "` + plugin.RuntimeRuncV2 + `" +[containerd.runtimes.runcv2.options] + BinaryName = "runc" + Root = "/runcv2" + NoNewKeyring = true +` + var nilOptsConfig, nonNilOptsConfig criconfig.Config + _, err := toml.Decode(nilOpts, &nilOptsConfig) + require.NoError(t, err) + _, err = toml.Decode(nonNilOpts, &nonNilOptsConfig) + require.NoError(t, err) + require.Len(t, nilOptsConfig.Runtimes, 3) + require.Len(t, nonNilOptsConfig.Runtimes, 3) + + for desc, test := range map[string]struct { + r criconfig.Runtime + c criconfig.Config + expectedOptions interface{} + }{ + "when options is nil, should return nil option for io.containerd.runc.v1": { + r: nilOptsConfig.Runtimes["runc"], + c: nilOptsConfig, + expectedOptions: nil, + }, + "when options is nil, should return nil option for io.containerd.runc.v2": { + r: nilOptsConfig.Runtimes["runcv2"], + c: nilOptsConfig, + expectedOptions: nil, + }, + "when options is nil, should use legacy fields for legacy runtime": { + r: nilOptsConfig.Runtimes["legacy"], + c: nilOptsConfig, + expectedOptions: &runctypes.RuncOptions{ + SystemdCgroup: true, + }, + }, + "when options is not nil, should be able to decode for io.containerd.runc.v1": { + r: nonNilOptsConfig.Runtimes["runc"], + c: nonNilOptsConfig, + expectedOptions: &runcoptions.Options{ + BinaryName: "runc", + Root: "/runc", + NoNewKeyring: true, + }, + }, + "when options is not nil, should be able to decode for io.containerd.runc.v2": { + r: nonNilOptsConfig.Runtimes["runcv2"], + c: nonNilOptsConfig, + expectedOptions: &runcoptions.Options{ + BinaryName: "runc", + Root: "/runcv2", + NoNewKeyring: true, + }, + }, + "when options is not nil, should be able to decode for legacy runtime": { + r: nonNilOptsConfig.Runtimes["legacy"], + c: nonNilOptsConfig, + expectedOptions: &runctypes.RuncOptions{ + Runtime: "legacy", + RuntimeRoot: "/legacy", + }, + }, + } { + t.Run(desc, func(t *testing.T) { + opts, err := generateRuntimeOptions(test.r, test.c) + assert.NoError(t, err) + assert.Equal(t, test.expectedOptions, opts) + }) + } +} + +func TestEnvDeduplication(t *testing.T) { + for desc, test := range map[string]struct { + existing []string + kv [][2]string + expected []string + }{ + "single env": { + kv: [][2]string{ + {"a", "b"}, + }, + expected: []string{"a=b"}, + }, + "multiple envs": { + kv: [][2]string{ + {"a", "b"}, + {"c", "d"}, + {"e", "f"}, + }, + expected: []string{ + "a=b", + "c=d", + "e=f", + }, + }, + "env override": { + kv: [][2]string{ + {"k1", "v1"}, + {"k2", "v2"}, + {"k3", "v3"}, + {"k3", "v4"}, + {"k1", "v5"}, + {"k4", "v6"}, + }, + expected: []string{ + "k1=v5", + "k2=v2", + "k3=v4", + "k4=v6", + }, + }, + "existing env": { + existing: []string{ + "k1=v1", + "k2=v2", + "k3=v3", + }, + kv: [][2]string{ + {"k3", "v4"}, + {"k2", "v5"}, + {"k4", "v6"}, + }, + expected: []string{ + "k1=v1", + "k2=v5", + "k3=v4", + "k4=v6", + }, + }, + } { + t.Logf("TestCase %q", desc) + var spec runtimespec.Spec + if len(test.existing) > 0 { + spec.Process = &runtimespec.Process{ + Env: test.existing, + } + } + for _, kv := range test.kv { + oci.WithEnv([]string{kv[0] + "=" + kv[1]})(context.Background(), nil, nil, &spec) + } + assert.Equal(t, test.expected, spec.Process.Env) + } +} + +func TestPassThroughAnnotationsFilter(t *testing.T) { + for desc, test := range map[string]struct { + podAnnotations map[string]string + runtimePodAnnotations []string + passthroughAnnotations map[string]string + }{ + "should support direct match": { + podAnnotations: map[string]string{"c": "d", "d": "e"}, + runtimePodAnnotations: []string{"c"}, + passthroughAnnotations: map[string]string{"c": "d"}, + }, + "should support wildcard match": { + podAnnotations: map[string]string{ + "t.f": "j", + "z.g": "o", + "z": "o", + "y.ca": "b", + "y": "b", + }, + runtimePodAnnotations: []string{"*.f", "z*g", "y.c*"}, + passthroughAnnotations: map[string]string{ + "t.f": "j", + "z.g": "o", + "y.ca": "b", + }, + }, + "should support wildcard match all": { + podAnnotations: map[string]string{ + "t.f": "j", + "z.g": "o", + "z": "o", + "y.ca": "b", + "y": "b", + }, + runtimePodAnnotations: []string{"*"}, + passthroughAnnotations: map[string]string{ + "t.f": "j", + "z.g": "o", + "z": "o", + "y.ca": "b", + "y": "b", + }, + }, + "should support match including path separator": { + podAnnotations: map[string]string{ + "matchend.com/end": "1", + "matchend.com/end1": "2", + "matchend.com/1end": "3", + "matchmid.com/mid": "4", + "matchmid.com/mi1d": "5", + "matchmid.com/mid1": "6", + "matchhead.com/head": "7", + "matchhead.com/1head": "8", + "matchhead.com/head1": "9", + "matchall.com/abc": "10", + "matchall.com/def": "11", + "end/matchend": "12", + "end1/matchend": "13", + "1end/matchend": "14", + "mid/matchmid": "15", + "mi1d/matchmid": "16", + "mid1/matchmid": "17", + "head/matchhead": "18", + "1head/matchhead": "19", + "head1/matchhead": "20", + "abc/matchall": "21", + "def/matchall": "22", + "match1/match2": "23", + "nomatch/nomatch": "24", + }, + runtimePodAnnotations: []string{ + "matchend.com/end*", + "matchmid.com/mi*d", + "matchhead.com/*head", + "matchall.com/*", + "end*/matchend", + "mi*d/matchmid", + "*head/matchhead", + "*/matchall", + "match*/match*", + }, + passthroughAnnotations: map[string]string{ + "matchend.com/end": "1", + "matchend.com/end1": "2", + "matchmid.com/mid": "4", + "matchmid.com/mi1d": "5", + "matchhead.com/head": "7", + "matchhead.com/1head": "8", + "matchall.com/abc": "10", + "matchall.com/def": "11", + "end/matchend": "12", + "end1/matchend": "13", + "mid/matchmid": "15", + "mi1d/matchmid": "16", + "head/matchhead": "18", + "1head/matchhead": "19", + "abc/matchall": "21", + "def/matchall": "22", + "match1/match2": "23", + }, + }, + } { + t.Run(desc, func(t *testing.T) { + passthroughAnnotations := getPassthroughAnnotations(test.podAnnotations, test.runtimePodAnnotations) + assert.Equal(t, test.passthroughAnnotations, passthroughAnnotations) + }) + } +} + +func TestEnsureRemoveAllNotExist(t *testing.T) { + // should never return an error for a non-existent path + if err := ensureRemoveAll(context.Background(), "/non/existent/path"); err != nil { + t.Fatal(err) + } +} + +func TestEnsureRemoveAllWithDir(t *testing.T) { + dir, err := ioutil.TempDir("", "test-ensure-removeall-with-dir") + if err != nil { + t.Fatal(err) + } + if err := ensureRemoveAll(context.Background(), dir); err != nil { + t.Fatal(err) + } +} + +func TestEnsureRemoveAllWithFile(t *testing.T) { + tmp, err := ioutil.TempFile("", "test-ensure-removeall-with-dir") + if err != nil { + t.Fatal(err) + } + tmp.Close() + if err := ensureRemoveAll(context.Background(), tmp.Name()); err != nil { + t.Fatal(err) + } +} diff --git a/vendor/github.com/containerd/cri/pkg/server/helpers_windows.go b/pkg/server/helpers_windows.go similarity index 80% rename from vendor/github.com/containerd/cri/pkg/server/helpers_windows.go rename to pkg/server/helpers_windows.go index 5ce7104da..f88f34bad 100644 --- a/vendor/github.com/containerd/cri/pkg/server/helpers_windows.go +++ b/pkg/server/helpers_windows.go @@ -23,7 +23,6 @@ import ( "os" "path/filepath" "syscall" - "time" "github.com/opencontainers/runtime-spec/specs-go" ) @@ -161,63 +160,9 @@ func fixLongPath(path string) string { return string(pathbuf[:w]) } -// ensureRemoveAll wraps `os.RemoveAll` to check for specific errors that can -// often be remedied. -// Only use `ensureRemoveAll` if you really want to make every effort to remove -// a directory. -// -// Because of the way `os.Remove` (and by extension `os.RemoveAll`) works, there -// can be a race between reading directory entries and then actually attempting -// to remove everything in the directory. -// These types of errors do not need to be returned since it's ok for the dir to -// be gone we can just retry the remove operation. -// -// This should not return a `os.ErrNotExist` kind of error under any circumstances +// ensureRemoveAll is a wrapper for os.RemoveAll on Windows. func ensureRemoveAll(_ context.Context, dir string) error { - notExistErr := make(map[string]bool) - - // track retries - exitOnErr := make(map[string]int) - maxRetry := 50 - - for { - err := os.RemoveAll(dir) - if err == nil { - return nil - } - - pe, ok := err.(*os.PathError) - if !ok { - return err - } - - if os.IsNotExist(err) { - if notExistErr[pe.Path] { - return err - } - notExistErr[pe.Path] = true - - // There is a race where some subdir can be removed but after the - // parent dir entries have been read. - // So the path could be from `os.Remove(subdir)` - // If the reported non-existent path is not the passed in `dir` we - // should just retry, but otherwise return with no error. - if pe.Path == dir { - return nil - } - continue - } - - if pe.Err != syscall.EBUSY { - return err - } - - if exitOnErr[pe.Path] == maxRetry { - return err - } - exitOnErr[pe.Path]++ - time.Sleep(100 * time.Millisecond) - } + return os.RemoveAll(dir) } func modifyProcessLabel(runtimeType string, spec *specs.Spec) error { diff --git a/vendor/github.com/containerd/cri/pkg/server/image_list.go b/pkg/server/image_list.go similarity index 100% rename from vendor/github.com/containerd/cri/pkg/server/image_list.go rename to pkg/server/image_list.go diff --git a/pkg/server/image_list_test.go b/pkg/server/image_list_test.go new file mode 100644 index 000000000..c630f19d2 --- /dev/null +++ b/pkg/server/image_list_test.go @@ -0,0 +1,113 @@ +/* + Copyright The containerd 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 + + http://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. +*/ + +package server + +import ( + "testing" + + imagespec "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/net/context" + runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" + + imagestore "github.com/containerd/containerd/pkg/store/image" +) + +func TestListImages(t *testing.T) { + c := newTestCRIService() + imagesInStore := []imagestore.Image{ + { + ID: "sha256:1123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + ChainID: "test-chainid-1", + References: []string{ + "gcr.io/library/busybox:latest", + "gcr.io/library/busybox@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582", + }, + Size: 1000, + ImageSpec: imagespec.Image{ + Config: imagespec.ImageConfig{ + User: "root", + }, + }, + }, + { + ID: "sha256:2123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + ChainID: "test-chainid-2", + References: []string{ + "gcr.io/library/alpine:latest", + "gcr.io/library/alpine@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582", + }, + Size: 2000, + ImageSpec: imagespec.Image{ + Config: imagespec.ImageConfig{ + User: "1234:1234", + }, + }, + }, + { + ID: "sha256:3123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + ChainID: "test-chainid-3", + References: []string{ + "gcr.io/library/ubuntu:latest", + "gcr.io/library/ubuntu@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582", + }, + Size: 3000, + ImageSpec: imagespec.Image{ + Config: imagespec.ImageConfig{ + User: "nobody", + }, + }, + }, + } + expect := []*runtime.Image{ + { + Id: "sha256:1123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + RepoTags: []string{"gcr.io/library/busybox:latest"}, + RepoDigests: []string{"gcr.io/library/busybox@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582"}, + Size_: uint64(1000), + Username: "root", + }, + { + Id: "sha256:2123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + RepoTags: []string{"gcr.io/library/alpine:latest"}, + RepoDigests: []string{"gcr.io/library/alpine@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582"}, + Size_: uint64(2000), + Uid: &runtime.Int64Value{Value: 1234}, + }, + { + Id: "sha256:3123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + RepoTags: []string{"gcr.io/library/ubuntu:latest"}, + RepoDigests: []string{"gcr.io/library/ubuntu@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582"}, + Size_: uint64(3000), + Username: "nobody", + }, + } + + var err error + c.imageStore, err = imagestore.NewFakeStore(imagesInStore) + assert.NoError(t, err) + + resp, err := c.ListImages(context.Background(), &runtime.ListImagesRequest{}) + assert.NoError(t, err) + require.NotNil(t, resp) + images := resp.GetImages() + assert.Len(t, images, len(expect)) + for _, i := range expect { + assert.Contains(t, images, i) + } +} diff --git a/vendor/github.com/containerd/cri/pkg/server/image_pull.go b/pkg/server/image_pull.go similarity index 99% rename from vendor/github.com/containerd/cri/pkg/server/image_pull.go rename to pkg/server/image_pull.go index 8e2493613..d6627bd04 100644 --- a/vendor/github.com/containerd/cri/pkg/server/image_pull.go +++ b/pkg/server/image_pull.go @@ -42,7 +42,7 @@ import ( "golang.org/x/net/context" runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" - criconfig "github.com/containerd/cri/pkg/config" + criconfig "github.com/containerd/containerd/pkg/config" ) // For image management: diff --git a/pkg/server/image_pull_test.go b/pkg/server/image_pull_test.go new file mode 100644 index 000000000..c41039c1a --- /dev/null +++ b/pkg/server/image_pull_test.go @@ -0,0 +1,379 @@ +/* + Copyright The containerd 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 + + http://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. +*/ + +package server + +import ( + "context" + "encoding/base64" + "fmt" + "strings" + "testing" + + digest "github.com/opencontainers/go-digest" + imagespec "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/stretchr/testify/assert" + runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" + + criconfig "github.com/containerd/containerd/pkg/config" +) + +func TestParseAuth(t *testing.T) { + testUser := "username" + testPasswd := "password" + testAuthLen := base64.StdEncoding.EncodedLen(len(testUser + ":" + testPasswd)) + testAuth := make([]byte, testAuthLen) + base64.StdEncoding.Encode(testAuth, []byte(testUser+":"+testPasswd)) + invalidAuth := make([]byte, testAuthLen) + base64.StdEncoding.Encode(invalidAuth, []byte(testUser+"@"+testPasswd)) + for desc, test := range map[string]struct { + auth *runtime.AuthConfig + host string + expectedUser string + expectedSecret string + expectErr bool + }{ + "should not return error if auth config is nil": {}, + "should not return error if empty auth is provided for access to anonymous registry": { + auth: &runtime.AuthConfig{}, + expectErr: false, + }, + "should support identity token": { + auth: &runtime.AuthConfig{IdentityToken: "abcd"}, + expectedSecret: "abcd", + }, + "should support username and password": { + auth: &runtime.AuthConfig{ + Username: testUser, + Password: testPasswd, + }, + expectedUser: testUser, + expectedSecret: testPasswd, + }, + "should support auth": { + auth: &runtime.AuthConfig{Auth: string(testAuth)}, + expectedUser: testUser, + expectedSecret: testPasswd, + }, + "should return error for invalid auth": { + auth: &runtime.AuthConfig{Auth: string(invalidAuth)}, + expectErr: true, + }, + "should return empty auth if server address doesn't match": { + auth: &runtime.AuthConfig{ + Username: testUser, + Password: testPasswd, + ServerAddress: "https://registry-1.io", + }, + host: "registry-2.io", + expectedUser: "", + expectedSecret: "", + }, + "should return auth if server address matches": { + auth: &runtime.AuthConfig{ + Username: testUser, + Password: testPasswd, + ServerAddress: "https://registry-1.io", + }, + host: "registry-1.io", + expectedUser: testUser, + expectedSecret: testPasswd, + }, + "should return auth if server address is not specified": { + auth: &runtime.AuthConfig{ + Username: testUser, + Password: testPasswd, + }, + host: "registry-1.io", + expectedUser: testUser, + expectedSecret: testPasswd, + }, + } { + t.Logf("TestCase %q", desc) + u, s, err := ParseAuth(test.auth, test.host) + assert.Equal(t, test.expectErr, err != nil) + assert.Equal(t, test.expectedUser, u) + assert.Equal(t, test.expectedSecret, s) + } +} + +func TestRegistryEndpoints(t *testing.T) { + for desc, test := range map[string]struct { + mirrors map[string]criconfig.Mirror + host string + expected []string + }{ + "no mirror configured": { + mirrors: map[string]criconfig.Mirror{ + "registry-1.io": { + Endpoints: []string{ + "https://registry-1.io", + "https://registry-2.io", + }, + }, + }, + host: "registry-3.io", + expected: []string{ + "https://registry-3.io", + }, + }, + "mirror configured": { + mirrors: map[string]criconfig.Mirror{ + "registry-3.io": { + Endpoints: []string{ + "https://registry-1.io", + "https://registry-2.io", + }, + }, + }, + host: "registry-3.io", + expected: []string{ + "https://registry-1.io", + "https://registry-2.io", + "https://registry-3.io", + }, + }, + "wildcard mirror configured": { + mirrors: map[string]criconfig.Mirror{ + "*": { + Endpoints: []string{ + "https://registry-1.io", + "https://registry-2.io", + }, + }, + }, + host: "registry-3.io", + expected: []string{ + "https://registry-1.io", + "https://registry-2.io", + "https://registry-3.io", + }, + }, + "host should take precedence if both host and wildcard mirrors are configured": { + mirrors: map[string]criconfig.Mirror{ + "*": { + Endpoints: []string{ + "https://registry-1.io", + }, + }, + "registry-3.io": { + Endpoints: []string{ + "https://registry-2.io", + }, + }, + }, + host: "registry-3.io", + expected: []string{ + "https://registry-2.io", + "https://registry-3.io", + }, + }, + "default endpoint in list with http": { + mirrors: map[string]criconfig.Mirror{ + "registry-3.io": { + Endpoints: []string{ + "https://registry-1.io", + "https://registry-2.io", + "http://registry-3.io", + }, + }, + }, + host: "registry-3.io", + expected: []string{ + "https://registry-1.io", + "https://registry-2.io", + "http://registry-3.io", + }, + }, + "default endpoint in list with https": { + mirrors: map[string]criconfig.Mirror{ + "registry-3.io": { + Endpoints: []string{ + "https://registry-1.io", + "https://registry-2.io", + "https://registry-3.io", + }, + }, + }, + host: "registry-3.io", + expected: []string{ + "https://registry-1.io", + "https://registry-2.io", + "https://registry-3.io", + }, + }, + "default endpoint in list with path": { + mirrors: map[string]criconfig.Mirror{ + "registry-3.io": { + Endpoints: []string{ + "https://registry-1.io", + "https://registry-2.io", + "https://registry-3.io/path", + }, + }, + }, + host: "registry-3.io", + expected: []string{ + "https://registry-1.io", + "https://registry-2.io", + "https://registry-3.io/path", + }, + }, + "miss scheme endpoint in list with path": { + mirrors: map[string]criconfig.Mirror{ + "registry-3.io": { + Endpoints: []string{ + "https://registry-3.io", + "registry-1.io", + "127.0.0.1:1234", + }, + }, + }, + host: "registry-3.io", + expected: []string{ + "https://registry-3.io", + "https://registry-1.io", + "http://127.0.0.1:1234", + }, + }, + } { + t.Logf("TestCase %q", desc) + c := newTestCRIService() + c.config.Registry.Mirrors = test.mirrors + got, err := c.registryEndpoints(test.host) + assert.NoError(t, err) + assert.Equal(t, test.expected, got) + } +} + +func TestDefaultScheme(t *testing.T) { + for desc, test := range map[string]struct { + host string + expected string + }{ + "should use http by default for localhost": { + host: "localhost", + expected: "http", + }, + "should use http by default for localhost with port": { + host: "localhost:8080", + expected: "http", + }, + "should use http by default for 127.0.0.1": { + host: "127.0.0.1", + expected: "http", + }, + "should use http by default for 127.0.0.1 with port": { + host: "127.0.0.1:8080", + expected: "http", + }, + "should use http by default for ::1": { + host: "::1", + expected: "http", + }, + "should use http by default for ::1 with port": { + host: "[::1]:8080", + expected: "http", + }, + "should use https by default for remote host": { + host: "remote", + expected: "https", + }, + "should use https by default for remote host with port": { + host: "remote:8080", + expected: "https", + }, + "should use https by default for remote ip": { + host: "8.8.8.8", + expected: "https", + }, + "should use https by default for remote ip with port": { + host: "8.8.8.8:8080", + expected: "https", + }, + } { + t.Logf("TestCase %q", desc) + got := defaultScheme(test.host) + assert.Equal(t, test.expected, got) + } +} + +func TestEncryptedImagePullOpts(t *testing.T) { + for desc, test := range map[string]struct { + keyModel string + expectedOpts int + }{ + "node key model should return one unpack opt": { + keyModel: criconfig.KeyModelNode, + expectedOpts: 1, + }, + "no key model selected should default to node key model": { + keyModel: "", + expectedOpts: 0, + }, + } { + t.Logf("TestCase %q", desc) + c := newTestCRIService() + c.config.ImageDecryption.KeyModel = test.keyModel + got := len(c.encryptedImagesPullOpts()) + assert.Equal(t, test.expectedOpts, got) + } +} + +func TestImageLayersLabel(t *testing.T) { + sampleKey := "sampleKey" + sampleDigest, err := digest.Parse("sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + assert.NoError(t, err) + sampleMaxSize := 300 + sampleValidate := func(k, v string) error { + if (len(k) + len(v)) > sampleMaxSize { + return fmt.Errorf("invalid: %q: %q", k, v) + } + return nil + } + + tests := []struct { + name string + layersNum int + wantNum int + }{ + { + name: "valid number of layers", + layersNum: 2, + wantNum: 2, + }, + { + name: "many layers", + layersNum: 5, // hits sampleMaxSize (300 chars). + wantNum: 4, // layers should be omitted for avoiding invalid label. + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var sampleLayers []imagespec.Descriptor + for i := 0; i < tt.layersNum; i++ { + sampleLayers = append(sampleLayers, imagespec.Descriptor{ + MediaType: imagespec.MediaTypeImageLayerGzip, + Digest: sampleDigest, + }) + } + gotS := getLayers(context.Background(), sampleKey, sampleLayers, sampleValidate) + got := len(strings.Split(gotS, ",")) + assert.Equal(t, tt.wantNum, got) + }) + } +} diff --git a/vendor/github.com/containerd/cri/pkg/server/image_remove.go b/pkg/server/image_remove.go similarity index 98% rename from vendor/github.com/containerd/cri/pkg/server/image_remove.go rename to pkg/server/image_remove.go index bcd02d758..ec54b9d55 100644 --- a/vendor/github.com/containerd/cri/pkg/server/image_remove.go +++ b/pkg/server/image_remove.go @@ -23,7 +23,7 @@ import ( "golang.org/x/net/context" runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" - "github.com/containerd/cri/pkg/store" + "github.com/containerd/containerd/pkg/store" ) // RemoveImage removes the image. diff --git a/vendor/github.com/containerd/cri/pkg/server/image_status.go b/pkg/server/image_status.go similarity index 96% rename from vendor/github.com/containerd/cri/pkg/server/image_status.go rename to pkg/server/image_status.go index 5ada7b007..ad6502d38 100644 --- a/vendor/github.com/containerd/cri/pkg/server/image_status.go +++ b/pkg/server/image_status.go @@ -25,8 +25,8 @@ import ( "golang.org/x/net/context" runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" - "github.com/containerd/cri/pkg/store" - imagestore "github.com/containerd/cri/pkg/store/image" + "github.com/containerd/containerd/pkg/store" + imagestore "github.com/containerd/containerd/pkg/store/image" ) // ImageStatus returns the status of the image, returns nil if the image isn't present. diff --git a/pkg/server/image_status_test.go b/pkg/server/image_status_test.go new file mode 100644 index 000000000..f91b19588 --- /dev/null +++ b/pkg/server/image_status_test.go @@ -0,0 +1,74 @@ +/* + Copyright The containerd 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 + + http://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. +*/ + +package server + +import ( + "testing" + + imagespec "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/net/context" + runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" + + imagestore "github.com/containerd/containerd/pkg/store/image" +) + +func TestImageStatus(t *testing.T) { + testID := "sha256:d848ce12891bf78792cda4a23c58984033b0c397a55e93a1556202222ecc5ed4" + image := imagestore.Image{ + ID: testID, + ChainID: "test-chain-id", + References: []string{ + "gcr.io/library/busybox:latest", + "gcr.io/library/busybox@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582", + }, + Size: 1234, + ImageSpec: imagespec.Image{ + Config: imagespec.ImageConfig{ + User: "user:group", + }, + }, + } + expected := &runtime.Image{ + Id: testID, + RepoTags: []string{"gcr.io/library/busybox:latest"}, + RepoDigests: []string{"gcr.io/library/busybox@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582"}, + Size_: uint64(1234), + Username: "user", + } + + c := newTestCRIService() + t.Logf("should return nil image spec without error for non-exist image") + resp, err := c.ImageStatus(context.Background(), &runtime.ImageStatusRequest{ + Image: &runtime.ImageSpec{Image: testID}, + }) + assert.NoError(t, err) + require.NotNil(t, resp) + assert.Nil(t, resp.GetImage()) + + c.imageStore, err = imagestore.NewFakeStore([]imagestore.Image{image}) + assert.NoError(t, err) + + t.Logf("should return correct image status for exist image") + resp, err = c.ImageStatus(context.Background(), &runtime.ImageStatusRequest{ + Image: &runtime.ImageSpec{Image: testID}, + }) + assert.NoError(t, err) + assert.NotNil(t, resp) + assert.Equal(t, expected, resp.GetImage()) +} diff --git a/vendor/github.com/containerd/cri/pkg/server/imagefs_info.go b/pkg/server/imagefs_info.go similarity index 100% rename from vendor/github.com/containerd/cri/pkg/server/imagefs_info.go rename to pkg/server/imagefs_info.go diff --git a/pkg/server/imagefs_info_test.go b/pkg/server/imagefs_info_test.go new file mode 100644 index 000000000..bf4bdb267 --- /dev/null +++ b/pkg/server/imagefs_info_test.go @@ -0,0 +1,70 @@ +/* + Copyright The containerd 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 + + http://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. +*/ + +package server + +import ( + "testing" + + snapshot "github.com/containerd/containerd/snapshots" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/net/context" + runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" + + snapshotstore "github.com/containerd/containerd/pkg/store/snapshot" +) + +func TestImageFsInfo(t *testing.T) { + c := newTestCRIService() + snapshots := []snapshotstore.Snapshot{ + { + Key: "key1", + Kind: snapshot.KindActive, + Size: 10, + Inodes: 100, + Timestamp: 234567, + }, + { + Key: "key2", + Kind: snapshot.KindCommitted, + Size: 20, + Inodes: 200, + Timestamp: 123456, + }, + { + Key: "key3", + Kind: snapshot.KindView, + Size: 0, + Inodes: 0, + Timestamp: 345678, + }, + } + expected := &runtime.FilesystemUsage{ + Timestamp: 123456, + FsId: &runtime.FilesystemIdentifier{Mountpoint: testImageFSPath}, + UsedBytes: &runtime.UInt64Value{Value: 30}, + InodesUsed: &runtime.UInt64Value{Value: 300}, + } + for _, sn := range snapshots { + c.snapshotStore.Add(sn) + } + resp, err := c.ImageFsInfo(context.Background(), &runtime.ImageFsInfoRequest{}) + require.NoError(t, err) + stats := resp.GetImageFilesystems() + assert.Len(t, stats, 1) + assert.Equal(t, expected, stats[0]) +} diff --git a/vendor/github.com/containerd/cri/pkg/server/instrumented_service.go b/pkg/server/instrumented_service.go similarity index 99% rename from vendor/github.com/containerd/cri/pkg/server/instrumented_service.go rename to pkg/server/instrumented_service.go index 2c2528ab6..9d3288c48 100644 --- a/vendor/github.com/containerd/cri/pkg/server/instrumented_service.go +++ b/pkg/server/instrumented_service.go @@ -24,7 +24,7 @@ import ( "golang.org/x/net/context" runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" - ctrdutil "github.com/containerd/cri/pkg/containerd/util" + ctrdutil "github.com/containerd/containerd/pkg/containerd/util" ) // instrumentedService wraps service with containerd namespace and logs. diff --git a/vendor/github.com/containerd/cri/pkg/server/io/container_io.go b/pkg/server/io/container_io.go similarity index 98% rename from vendor/github.com/containerd/cri/pkg/server/io/container_io.go rename to pkg/server/io/container_io.go index c66549ca5..edb929e93 100644 --- a/vendor/github.com/containerd/cri/pkg/server/io/container_io.go +++ b/pkg/server/io/container_io.go @@ -25,8 +25,8 @@ import ( "github.com/containerd/containerd/cio" "github.com/sirupsen/logrus" - cioutil "github.com/containerd/cri/pkg/ioutil" - "github.com/containerd/cri/pkg/util" + cioutil "github.com/containerd/containerd/pkg/ioutil" + "github.com/containerd/containerd/pkg/util" ) // streamKey generates a key for the stream. diff --git a/vendor/github.com/containerd/cri/pkg/server/io/exec_io.go b/pkg/server/io/exec_io.go similarity index 98% rename from vendor/github.com/containerd/cri/pkg/server/io/exec_io.go rename to pkg/server/io/exec_io.go index 4a695030d..667977fda 100644 --- a/vendor/github.com/containerd/cri/pkg/server/io/exec_io.go +++ b/pkg/server/io/exec_io.go @@ -23,7 +23,7 @@ import ( "github.com/containerd/containerd/cio" "github.com/sirupsen/logrus" - cioutil "github.com/containerd/cri/pkg/ioutil" + cioutil "github.com/containerd/containerd/pkg/ioutil" ) // ExecIO holds the exec io. diff --git a/vendor/github.com/containerd/cri/pkg/server/io/helpers.go b/pkg/server/io/helpers.go similarity index 100% rename from vendor/github.com/containerd/cri/pkg/server/io/helpers.go rename to pkg/server/io/helpers.go diff --git a/vendor/github.com/containerd/cri/pkg/server/io/helpers_unix.go b/pkg/server/io/helpers_unix.go similarity index 100% rename from vendor/github.com/containerd/cri/pkg/server/io/helpers_unix.go rename to pkg/server/io/helpers_unix.go diff --git a/vendor/github.com/containerd/cri/pkg/server/io/helpers_windows.go b/pkg/server/io/helpers_windows.go similarity index 100% rename from vendor/github.com/containerd/cri/pkg/server/io/helpers_windows.go rename to pkg/server/io/helpers_windows.go diff --git a/vendor/github.com/containerd/cri/pkg/server/io/logger.go b/pkg/server/io/logger.go similarity index 99% rename from vendor/github.com/containerd/cri/pkg/server/io/logger.go rename to pkg/server/io/logger.go index f13b6f8bf..22d50db9d 100644 --- a/vendor/github.com/containerd/cri/pkg/server/io/logger.go +++ b/pkg/server/io/logger.go @@ -27,7 +27,7 @@ import ( "github.com/sirupsen/logrus" runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" - cioutil "github.com/containerd/cri/pkg/ioutil" + cioutil "github.com/containerd/containerd/pkg/ioutil" ) const ( diff --git a/pkg/server/io/logger_test.go b/pkg/server/io/logger_test.go new file mode 100644 index 000000000..f63e274a8 --- /dev/null +++ b/pkg/server/io/logger_test.go @@ -0,0 +1,258 @@ +/* + Copyright The containerd 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 + + http://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. +*/ + +package io + +import ( + "bytes" + "io/ioutil" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" + + cioutil "github.com/containerd/containerd/pkg/ioutil" +) + +func TestRedirectLogs(t *testing.T) { + // defaultBufSize is even number + const maxLen = defaultBufSize * 4 + for desc, test := range map[string]struct { + input string + stream StreamType + maxLen int + tag []runtime.LogTag + content []string + }{ + "stdout log": { + input: "test stdout log 1\ntest stdout log 2\n", + stream: Stdout, + maxLen: maxLen, + tag: []runtime.LogTag{ + runtime.LogTagFull, + runtime.LogTagFull, + }, + content: []string{ + "test stdout log 1", + "test stdout log 2", + }, + }, + "stderr log": { + input: "test stderr log 1\ntest stderr log 2\n", + stream: Stderr, + maxLen: maxLen, + tag: []runtime.LogTag{ + runtime.LogTagFull, + runtime.LogTagFull, + }, + content: []string{ + "test stderr log 1", + "test stderr log 2", + }, + }, + "log ends without newline": { + input: "test stderr log 1\ntest stderr log 2", + stream: Stderr, + maxLen: maxLen, + tag: []runtime.LogTag{ + runtime.LogTagFull, + runtime.LogTagPartial, + }, + content: []string{ + "test stderr log 1", + "test stderr log 2", + }, + }, + "log length equal to buffer size": { + input: strings.Repeat("a", defaultBufSize) + "\n" + strings.Repeat("a", defaultBufSize) + "\n", + stream: Stdout, + maxLen: maxLen, + tag: []runtime.LogTag{ + runtime.LogTagFull, + runtime.LogTagFull, + }, + content: []string{ + strings.Repeat("a", defaultBufSize), + strings.Repeat("a", defaultBufSize), + }, + }, + "log length longer than buffer size": { + input: strings.Repeat("a", defaultBufSize*2+10) + "\n" + strings.Repeat("a", defaultBufSize*2+20) + "\n", + stream: Stdout, + maxLen: maxLen, + tag: []runtime.LogTag{ + runtime.LogTagFull, + runtime.LogTagFull, + }, + content: []string{ + strings.Repeat("a", defaultBufSize*2+10), + strings.Repeat("a", defaultBufSize*2+20), + }, + }, + "log length equal to max length": { + input: strings.Repeat("a", maxLen) + "\n" + strings.Repeat("a", maxLen) + "\n", + stream: Stdout, + maxLen: maxLen, + tag: []runtime.LogTag{ + runtime.LogTagFull, + runtime.LogTagFull, + }, + content: []string{ + strings.Repeat("a", maxLen), + strings.Repeat("a", maxLen), + }, + }, + "log length exceed max length by 1": { + input: strings.Repeat("a", maxLen+1) + "\n" + strings.Repeat("a", maxLen+1) + "\n", + stream: Stdout, + maxLen: maxLen, + tag: []runtime.LogTag{ + runtime.LogTagPartial, + runtime.LogTagFull, + runtime.LogTagPartial, + runtime.LogTagFull, + }, + content: []string{ + strings.Repeat("a", maxLen), + "a", + strings.Repeat("a", maxLen), + "a", + }, + }, + "log length longer than max length": { + input: strings.Repeat("a", maxLen*2) + "\n" + strings.Repeat("a", maxLen*2+1) + "\n", + stream: Stdout, + maxLen: maxLen, + tag: []runtime.LogTag{ + runtime.LogTagPartial, + runtime.LogTagFull, + runtime.LogTagPartial, + runtime.LogTagPartial, + runtime.LogTagFull, + }, + content: []string{ + strings.Repeat("a", maxLen), + strings.Repeat("a", maxLen), + strings.Repeat("a", maxLen), + strings.Repeat("a", maxLen), + "a", + }, + }, + "max length shorter than buffer size": { + input: strings.Repeat("a", defaultBufSize*3/2+10) + "\n" + strings.Repeat("a", defaultBufSize*3/2+20) + "\n", + stream: Stdout, + maxLen: defaultBufSize / 2, + tag: []runtime.LogTag{ + runtime.LogTagPartial, + runtime.LogTagPartial, + runtime.LogTagPartial, + runtime.LogTagFull, + runtime.LogTagPartial, + runtime.LogTagPartial, + runtime.LogTagPartial, + runtime.LogTagFull, + }, + content: []string{ + strings.Repeat("a", defaultBufSize*1/2), + strings.Repeat("a", defaultBufSize*1/2), + strings.Repeat("a", defaultBufSize*1/2), + strings.Repeat("a", 10), + strings.Repeat("a", defaultBufSize*1/2), + strings.Repeat("a", defaultBufSize*1/2), + strings.Repeat("a", defaultBufSize*1/2), + strings.Repeat("a", 20), + }, + }, + "log length longer than max length, and (maxLen % defaultBufSize != 0)": { + input: strings.Repeat("a", defaultBufSize*2+10) + "\n" + strings.Repeat("a", defaultBufSize*2+20) + "\n", + stream: Stdout, + maxLen: defaultBufSize * 3 / 2, + tag: []runtime.LogTag{ + runtime.LogTagPartial, + runtime.LogTagFull, + runtime.LogTagPartial, + runtime.LogTagFull, + }, + content: []string{ + strings.Repeat("a", defaultBufSize*3/2), + strings.Repeat("a", defaultBufSize*1/2+10), + strings.Repeat("a", defaultBufSize*3/2), + strings.Repeat("a", defaultBufSize*1/2+20), + }, + }, + "no limit if max length is 0": { + input: strings.Repeat("a", defaultBufSize*10+10) + "\n" + strings.Repeat("a", defaultBufSize*10+20) + "\n", + stream: Stdout, + maxLen: 0, + tag: []runtime.LogTag{ + runtime.LogTagFull, + runtime.LogTagFull, + }, + content: []string{ + strings.Repeat("a", defaultBufSize*10+10), + strings.Repeat("a", defaultBufSize*10+20), + }, + }, + "no limit if max length is negative": { + input: strings.Repeat("a", defaultBufSize*10+10) + "\n" + strings.Repeat("a", defaultBufSize*10+20) + "\n", + stream: Stdout, + maxLen: -1, + tag: []runtime.LogTag{ + runtime.LogTagFull, + runtime.LogTagFull, + }, + content: []string{ + strings.Repeat("a", defaultBufSize*10+10), + strings.Repeat("a", defaultBufSize*10+20), + }, + }, + "log length longer than buffer size with tailing \\r\\n": { + input: strings.Repeat("a", defaultBufSize-1) + "\r\n" + strings.Repeat("a", defaultBufSize-1) + "\r\n", + stream: Stdout, + maxLen: -1, + tag: []runtime.LogTag{ + runtime.LogTagFull, + runtime.LogTagFull, + }, + content: []string{ + strings.Repeat("a", defaultBufSize-1), + strings.Repeat("a", defaultBufSize-1), + }, + }, + } { + t.Logf("TestCase %q", desc) + rc := ioutil.NopCloser(strings.NewReader(test.input)) + buf := bytes.NewBuffer(nil) + wc := cioutil.NewNopWriteCloser(buf) + redirectLogs("test-path", rc, wc, test.stream, test.maxLen) + output := buf.String() + lines := strings.Split(output, "\n") + lines = lines[:len(lines)-1] // Discard empty string after last \n + assert.Len(t, lines, len(test.content)) + for i := range lines { + fields := strings.SplitN(lines[i], string([]byte{delimiter}), 4) + require.Len(t, fields, 4) + _, err := time.Parse(timestampFormat, fields[0]) + assert.NoError(t, err) + assert.EqualValues(t, test.stream, fields[1]) + assert.Equal(t, string(test.tag[i]), fields[2]) + assert.Equal(t, test.content[i], fields[3]) + } + } +} diff --git a/vendor/github.com/containerd/cri/pkg/server/opts.go b/pkg/server/opts.go similarity index 100% rename from vendor/github.com/containerd/cri/pkg/server/opts.go rename to pkg/server/opts.go diff --git a/vendor/github.com/containerd/cri/pkg/server/restart.go b/pkg/server/restart.go similarity index 98% rename from vendor/github.com/containerd/cri/pkg/server/restart.go rename to pkg/server/restart.go index 2480bd5ea..fb3b3e174 100644 --- a/vendor/github.com/containerd/cri/pkg/server/restart.go +++ b/pkg/server/restart.go @@ -34,11 +34,11 @@ import ( "golang.org/x/net/context" runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" - ctrdutil "github.com/containerd/cri/pkg/containerd/util" - "github.com/containerd/cri/pkg/netns" - cio "github.com/containerd/cri/pkg/server/io" - containerstore "github.com/containerd/cri/pkg/store/container" - sandboxstore "github.com/containerd/cri/pkg/store/sandbox" + ctrdutil "github.com/containerd/containerd/pkg/containerd/util" + "github.com/containerd/containerd/pkg/netns" + cio "github.com/containerd/containerd/pkg/server/io" + containerstore "github.com/containerd/containerd/pkg/store/container" + sandboxstore "github.com/containerd/containerd/pkg/store/sandbox" ) // NOTE: The recovery logic has following assumption: when the cri plugin is down: diff --git a/vendor/github.com/containerd/cri/pkg/server/sandbox_list.go b/pkg/server/sandbox_list.go similarity index 97% rename from vendor/github.com/containerd/cri/pkg/server/sandbox_list.go rename to pkg/server/sandbox_list.go index d2528b267..50e552d7c 100644 --- a/vendor/github.com/containerd/cri/pkg/server/sandbox_list.go +++ b/pkg/server/sandbox_list.go @@ -20,7 +20,7 @@ import ( "golang.org/x/net/context" runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" - sandboxstore "github.com/containerd/cri/pkg/store/sandbox" + sandboxstore "github.com/containerd/containerd/pkg/store/sandbox" ) // ListPodSandbox returns a list of Sandbox. diff --git a/pkg/server/sandbox_list_test.go b/pkg/server/sandbox_list_test.go new file mode 100644 index 000000000..f22285889 --- /dev/null +++ b/pkg/server/sandbox_list_test.go @@ -0,0 +1,208 @@ +/* + Copyright The containerd 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 + + http://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. +*/ + +package server + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" + + sandboxstore "github.com/containerd/containerd/pkg/store/sandbox" +) + +func TestToCRISandbox(t *testing.T) { + config := &runtime.PodSandboxConfig{ + Metadata: &runtime.PodSandboxMetadata{ + Name: "test-name", + Uid: "test-uid", + Namespace: "test-ns", + Attempt: 1, + }, + Labels: map[string]string{"a": "b"}, + Annotations: map[string]string{"c": "d"}, + } + createdAt := time.Now() + meta := sandboxstore.Metadata{ + ID: "test-id", + Name: "test-name", + Config: config, + NetNSPath: "test-netns", + RuntimeHandler: "test-runtime-handler", + } + expect := &runtime.PodSandbox{ + Id: "test-id", + Metadata: config.GetMetadata(), + CreatedAt: createdAt.UnixNano(), + Labels: config.GetLabels(), + Annotations: config.GetAnnotations(), + RuntimeHandler: "test-runtime-handler", + } + for desc, test := range map[string]struct { + state sandboxstore.State + expectedState runtime.PodSandboxState + }{ + "sandbox state ready": { + state: sandboxstore.StateReady, + expectedState: runtime.PodSandboxState_SANDBOX_READY, + }, + "sandbox state not ready": { + state: sandboxstore.StateNotReady, + expectedState: runtime.PodSandboxState_SANDBOX_NOTREADY, + }, + "sandbox state unknown": { + state: sandboxstore.StateUnknown, + expectedState: runtime.PodSandboxState_SANDBOX_NOTREADY, + }, + } { + status := sandboxstore.Status{ + CreatedAt: createdAt, + State: test.state, + } + expect.State = test.expectedState + s := toCRISandbox(meta, status) + assert.Equal(t, expect, s, desc) + } +} + +func TestFilterSandboxes(t *testing.T) { + c := newTestCRIService() + sandboxes := []sandboxstore.Sandbox{ + sandboxstore.NewSandbox( + sandboxstore.Metadata{ + ID: "1abcdef", + Name: "sandboxname-1", + Config: &runtime.PodSandboxConfig{ + Metadata: &runtime.PodSandboxMetadata{ + Name: "podname-1", + Uid: "uid-1", + Namespace: "ns-1", + Attempt: 1, + }, + }, + RuntimeHandler: "test-runtime-handler", + }, + sandboxstore.Status{ + CreatedAt: time.Now(), + State: sandboxstore.StateReady, + }, + ), + sandboxstore.NewSandbox( + sandboxstore.Metadata{ + ID: "2abcdef", + Name: "sandboxname-2", + Config: &runtime.PodSandboxConfig{ + Metadata: &runtime.PodSandboxMetadata{ + Name: "podname-2", + Uid: "uid-2", + Namespace: "ns-2", + Attempt: 2, + }, + Labels: map[string]string{"a": "b"}, + }, + RuntimeHandler: "test-runtime-handler", + }, + sandboxstore.Status{ + CreatedAt: time.Now(), + State: sandboxstore.StateNotReady, + }, + ), + sandboxstore.NewSandbox( + sandboxstore.Metadata{ + ID: "3abcdef", + Name: "sandboxname-3", + Config: &runtime.PodSandboxConfig{ + Metadata: &runtime.PodSandboxMetadata{ + Name: "podname-2", + Uid: "uid-2", + Namespace: "ns-2", + Attempt: 2, + }, + Labels: map[string]string{"c": "d"}, + }, + RuntimeHandler: "test-runtime-handler", + }, + sandboxstore.Status{ + CreatedAt: time.Now(), + State: sandboxstore.StateReady, + }, + ), + } + + // Create PodSandbox + testSandboxes := []*runtime.PodSandbox{} + for _, sb := range sandboxes { + testSandboxes = append(testSandboxes, toCRISandbox(sb.Metadata, sb.Status.Get())) + } + + // Inject test sandbox metadata + for _, sb := range sandboxes { + assert.NoError(t, c.sandboxStore.Add(sb)) + } + + for desc, test := range map[string]struct { + filter *runtime.PodSandboxFilter + expect []*runtime.PodSandbox + }{ + "no filter": { + expect: testSandboxes, + }, + "id filter": { + filter: &runtime.PodSandboxFilter{Id: "2abcdef"}, + expect: []*runtime.PodSandbox{testSandboxes[1]}, + }, + "truncid filter": { + filter: &runtime.PodSandboxFilter{Id: "2"}, + expect: []*runtime.PodSandbox{testSandboxes[1]}, + }, + "state filter": { + filter: &runtime.PodSandboxFilter{ + State: &runtime.PodSandboxStateValue{ + State: runtime.PodSandboxState_SANDBOX_READY, + }, + }, + expect: []*runtime.PodSandbox{testSandboxes[0], testSandboxes[2]}, + }, + "label filter": { + filter: &runtime.PodSandboxFilter{ + LabelSelector: map[string]string{"a": "b"}, + }, + expect: []*runtime.PodSandbox{testSandboxes[1]}, + }, + "mixed filter not matched": { + filter: &runtime.PodSandboxFilter{ + Id: "1", + LabelSelector: map[string]string{"a": "b"}, + }, + expect: []*runtime.PodSandbox{}, + }, + "mixed filter matched": { + filter: &runtime.PodSandboxFilter{ + State: &runtime.PodSandboxStateValue{ + State: runtime.PodSandboxState_SANDBOX_READY, + }, + LabelSelector: map[string]string{"c": "d"}, + }, + expect: []*runtime.PodSandbox{testSandboxes[2]}, + }, + } { + t.Logf("TestCase: %s", desc) + filtered := c.filterCRISandboxes(testSandboxes, test.filter) + assert.Equal(t, test.expect, filtered, desc) + } +} diff --git a/vendor/github.com/containerd/cri/pkg/server/sandbox_portforward.go b/pkg/server/sandbox_portforward.go similarity index 95% rename from vendor/github.com/containerd/cri/pkg/server/sandbox_portforward.go rename to pkg/server/sandbox_portforward.go index 6d382ba2b..15d9ed635 100644 --- a/vendor/github.com/containerd/cri/pkg/server/sandbox_portforward.go +++ b/pkg/server/sandbox_portforward.go @@ -21,7 +21,7 @@ import ( "golang.org/x/net/context" runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" - sandboxstore "github.com/containerd/cri/pkg/store/sandbox" + sandboxstore "github.com/containerd/containerd/pkg/store/sandbox" ) // PortForward prepares a streaming endpoint to forward ports from a PodSandbox, and returns the address. diff --git a/vendor/github.com/containerd/cri/pkg/server/sandbox_portforward_unix.go b/pkg/server/sandbox_portforward_linux.go similarity index 99% rename from vendor/github.com/containerd/cri/pkg/server/sandbox_portforward_unix.go rename to pkg/server/sandbox_portforward_linux.go index 5691c2b61..32b062456 100644 --- a/vendor/github.com/containerd/cri/pkg/server/sandbox_portforward_unix.go +++ b/pkg/server/sandbox_portforward_linux.go @@ -1,5 +1,3 @@ -// +build !windows - /* Copyright The containerd Authors. diff --git a/pkg/server/sandbox_portforward_other.go b/pkg/server/sandbox_portforward_other.go new file mode 100644 index 000000000..1b88170ed --- /dev/null +++ b/pkg/server/sandbox_portforward_other.go @@ -0,0 +1,33 @@ +// +build !windows,!linux + +/* + Copyright The containerd 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 + + http://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. +*/ + +package server + +import ( + "io" + + "github.com/containerd/containerd/errdefs" + "github.com/pkg/errors" + "golang.org/x/net/context" +) + +// portForward uses netns to enter the sandbox namespace, and forwards a stream inside the +// the namespace to a specific port. It keeps forwarding until it exits or client disconnect. +func (c *criService) portForward(ctx context.Context, id string, port int32, stream io.ReadWriteCloser) error { + return errors.Wrap(errdefs.ErrNotImplemented, "port forward") +} diff --git a/vendor/github.com/containerd/cri/pkg/server/sandbox_portforward_windows.go b/pkg/server/sandbox_portforward_windows.go similarity index 95% rename from vendor/github.com/containerd/cri/pkg/server/sandbox_portforward_windows.go rename to pkg/server/sandbox_portforward_windows.go index 3c328a314..127ef2b50 100644 --- a/vendor/github.com/containerd/cri/pkg/server/sandbox_portforward_windows.go +++ b/pkg/server/sandbox_portforward_windows.go @@ -27,8 +27,8 @@ import ( "golang.org/x/net/context" "k8s.io/utils/exec" - "github.com/containerd/cri/pkg/ioutil" - sandboxstore "github.com/containerd/cri/pkg/store/sandbox" + "github.com/containerd/containerd/pkg/ioutil" + sandboxstore "github.com/containerd/containerd/pkg/store/sandbox" ) func (c *criService) portForward(ctx context.Context, id string, port int32, stream io.ReadWriter) error { diff --git a/vendor/github.com/containerd/cri/pkg/server/sandbox_remove.go b/pkg/server/sandbox_remove.go similarity index 97% rename from vendor/github.com/containerd/cri/pkg/server/sandbox_remove.go rename to pkg/server/sandbox_remove.go index 2c2deb2e0..929528664 100644 --- a/vendor/github.com/containerd/cri/pkg/server/sandbox_remove.go +++ b/pkg/server/sandbox_remove.go @@ -25,8 +25,8 @@ import ( "golang.org/x/net/context" runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" - "github.com/containerd/cri/pkg/store" - sandboxstore "github.com/containerd/cri/pkg/store/sandbox" + "github.com/containerd/containerd/pkg/store" + sandboxstore "github.com/containerd/containerd/pkg/store/sandbox" ) // RemovePodSandbox removes the sandbox. If there are running containers in the diff --git a/vendor/github.com/containerd/cri/pkg/server/sandbox_run.go b/pkg/server/sandbox_run.go similarity index 97% rename from vendor/github.com/containerd/cri/pkg/server/sandbox_run.go rename to pkg/server/sandbox_run.go index e0b207d7a..cc6e8d6fa 100644 --- a/vendor/github.com/containerd/cri/pkg/server/sandbox_run.go +++ b/pkg/server/sandbox_run.go @@ -36,20 +36,20 @@ import ( "golang.org/x/net/context" runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" - "github.com/containerd/cri/pkg/annotations" - criconfig "github.com/containerd/cri/pkg/config" - customopts "github.com/containerd/cri/pkg/containerd/opts" - ctrdutil "github.com/containerd/cri/pkg/containerd/util" - "github.com/containerd/cri/pkg/netns" - "github.com/containerd/cri/pkg/server/bandwidth" - sandboxstore "github.com/containerd/cri/pkg/store/sandbox" - "github.com/containerd/cri/pkg/util" + "github.com/containerd/containerd/pkg/annotations" + criconfig "github.com/containerd/containerd/pkg/config" + customopts "github.com/containerd/containerd/pkg/containerd/opts" + ctrdutil "github.com/containerd/containerd/pkg/containerd/util" + "github.com/containerd/containerd/pkg/netns" + "github.com/containerd/containerd/pkg/server/bandwidth" + sandboxstore "github.com/containerd/containerd/pkg/store/sandbox" + "github.com/containerd/containerd/pkg/util" selinux "github.com/opencontainers/selinux/go-selinux" ) func init() { typeurl.Register(&sandboxstore.Metadata{}, - "github.com/containerd/cri/pkg/store/sandbox", "Metadata") + "github.com/containerd/containerd/pkg/store/sandbox", "Metadata") } // RunPodSandbox creates and starts a pod-level sandbox. Runtimes should ensure diff --git a/vendor/github.com/containerd/cri/pkg/server/sandbox_run_unix.go b/pkg/server/sandbox_run_linux.go similarity index 98% rename from vendor/github.com/containerd/cri/pkg/server/sandbox_run_unix.go rename to pkg/server/sandbox_run_linux.go index ad0b85254..ee506470f 100644 --- a/vendor/github.com/containerd/cri/pkg/server/sandbox_run_unix.go +++ b/pkg/server/sandbox_run_linux.go @@ -1,5 +1,3 @@ -// +build !windows - /* Copyright The containerd Authors. @@ -33,9 +31,9 @@ import ( "golang.org/x/sys/unix" runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" - "github.com/containerd/cri/pkg/annotations" - customopts "github.com/containerd/cri/pkg/containerd/opts" - osinterface "github.com/containerd/cri/pkg/os" + "github.com/containerd/containerd/pkg/annotations" + customopts "github.com/containerd/containerd/pkg/containerd/opts" + osinterface "github.com/containerd/containerd/pkg/os" ) func (c *criService) sandboxContainerSpec(id string, config *runtime.PodSandboxConfig, diff --git a/pkg/server/sandbox_run_linux_test.go b/pkg/server/sandbox_run_linux_test.go new file mode 100644 index 000000000..78f21d0c2 --- /dev/null +++ b/pkg/server/sandbox_run_linux_test.go @@ -0,0 +1,431 @@ +/* + Copyright The containerd 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 + + http://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. +*/ + +package server + +import ( + "os" + "path/filepath" + "testing" + + imagespec "github.com/opencontainers/image-spec/specs-go/v1" + runtimespec "github.com/opencontainers/runtime-spec/specs-go" + "github.com/opencontainers/selinux/go-selinux" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" + + "github.com/containerd/containerd/pkg/annotations" + "github.com/containerd/containerd/pkg/containerd/opts" + ostesting "github.com/containerd/containerd/pkg/os/testing" +) + +func getRunPodSandboxTestData() (*runtime.PodSandboxConfig, *imagespec.ImageConfig, func(*testing.T, string, *runtimespec.Spec)) { + config := &runtime.PodSandboxConfig{ + Metadata: &runtime.PodSandboxMetadata{ + Name: "test-name", + Uid: "test-uid", + Namespace: "test-ns", + Attempt: 1, + }, + Hostname: "test-hostname", + LogDirectory: "test-log-directory", + Labels: map[string]string{"a": "b"}, + Annotations: map[string]string{"c": "d"}, + Linux: &runtime.LinuxPodSandboxConfig{ + CgroupParent: "/test/cgroup/parent", + }, + } + imageConfig := &imagespec.ImageConfig{ + Env: []string{"a=b", "c=d"}, + Entrypoint: []string{"/pause"}, + Cmd: []string{"forever"}, + WorkingDir: "/workspace", + } + specCheck := func(t *testing.T, id string, spec *runtimespec.Spec) { + assert.Equal(t, "test-hostname", spec.Hostname) + assert.Equal(t, getCgroupsPath("/test/cgroup/parent", id), spec.Linux.CgroupsPath) + assert.Equal(t, relativeRootfsPath, spec.Root.Path) + assert.Equal(t, true, spec.Root.Readonly) + assert.Contains(t, spec.Process.Env, "a=b", "c=d") + assert.Equal(t, []string{"/pause", "forever"}, spec.Process.Args) + assert.Equal(t, "/workspace", spec.Process.Cwd) + assert.EqualValues(t, *spec.Linux.Resources.CPU.Shares, opts.DefaultSandboxCPUshares) + assert.EqualValues(t, *spec.Process.OOMScoreAdj, defaultSandboxOOMAdj) + + t.Logf("Check PodSandbox annotations") + assert.Contains(t, spec.Annotations, annotations.SandboxID) + assert.EqualValues(t, spec.Annotations[annotations.SandboxID], id) + + assert.Contains(t, spec.Annotations, annotations.ContainerType) + assert.EqualValues(t, spec.Annotations[annotations.ContainerType], annotations.ContainerTypeSandbox) + + assert.Contains(t, spec.Annotations, annotations.SandboxLogDir) + assert.EqualValues(t, spec.Annotations[annotations.SandboxLogDir], "test-log-directory") + + if selinux.GetEnabled() { + assert.NotEqual(t, "", spec.Process.SelinuxLabel) + assert.NotEqual(t, "", spec.Linux.MountLabel) + } + } + return config, imageConfig, specCheck +} + +func TestLinuxSandboxContainerSpec(t *testing.T) { + testID := "test-id" + nsPath := "test-cni" + for desc, test := range map[string]struct { + configChange func(*runtime.PodSandboxConfig) + specCheck func(*testing.T, *runtimespec.Spec) + expectErr bool + }{ + "spec should reflect original config": { + specCheck: func(t *testing.T, spec *runtimespec.Spec) { + // runtime spec should have expected namespaces enabled by default. + require.NotNil(t, spec.Linux) + assert.Contains(t, spec.Linux.Namespaces, runtimespec.LinuxNamespace{ + Type: runtimespec.NetworkNamespace, + Path: nsPath, + }) + assert.Contains(t, spec.Linux.Namespaces, runtimespec.LinuxNamespace{ + Type: runtimespec.UTSNamespace, + }) + assert.Contains(t, spec.Linux.Namespaces, runtimespec.LinuxNamespace{ + Type: runtimespec.PIDNamespace, + }) + assert.Contains(t, spec.Linux.Namespaces, runtimespec.LinuxNamespace{ + Type: runtimespec.IPCNamespace, + }) + }, + }, + "host namespace": { + configChange: func(c *runtime.PodSandboxConfig) { + c.Linux.SecurityContext = &runtime.LinuxSandboxSecurityContext{ + NamespaceOptions: &runtime.NamespaceOption{ + Network: runtime.NamespaceMode_NODE, + Pid: runtime.NamespaceMode_NODE, + Ipc: runtime.NamespaceMode_NODE, + }, + } + }, + specCheck: func(t *testing.T, spec *runtimespec.Spec) { + // runtime spec should disable expected namespaces in host mode. + require.NotNil(t, spec.Linux) + assert.NotContains(t, spec.Linux.Namespaces, runtimespec.LinuxNamespace{ + Type: runtimespec.NetworkNamespace, + }) + assert.NotContains(t, spec.Linux.Namespaces, runtimespec.LinuxNamespace{ + Type: runtimespec.UTSNamespace, + }) + assert.NotContains(t, spec.Linux.Namespaces, runtimespec.LinuxNamespace{ + Type: runtimespec.PIDNamespace, + }) + assert.NotContains(t, spec.Linux.Namespaces, runtimespec.LinuxNamespace{ + Type: runtimespec.IPCNamespace, + }) + }, + }, + "should set supplemental groups correctly": { + configChange: func(c *runtime.PodSandboxConfig) { + c.Linux.SecurityContext = &runtime.LinuxSandboxSecurityContext{ + SupplementalGroups: []int64{1111, 2222}, + } + }, + specCheck: func(t *testing.T, spec *runtimespec.Spec) { + require.NotNil(t, spec.Process) + assert.Contains(t, spec.Process.User.AdditionalGids, uint32(1111)) + assert.Contains(t, spec.Process.User.AdditionalGids, uint32(2222)) + }, + }, + } { + t.Logf("TestCase %q", desc) + c := newTestCRIService() + config, imageConfig, specCheck := getRunPodSandboxTestData() + if test.configChange != nil { + test.configChange(config) + } + spec, err := c.sandboxContainerSpec(testID, config, imageConfig, nsPath, nil) + if test.expectErr { + assert.Error(t, err) + assert.Nil(t, spec) + continue + } + assert.NoError(t, err) + assert.NotNil(t, spec) + specCheck(t, testID, spec) + if test.specCheck != nil { + test.specCheck(t, spec) + } + } +} + +func TestSetupSandboxFiles(t *testing.T) { + const ( + testID = "test-id" + realhostname = "test-real-hostname" + ) + for desc, test := range map[string]struct { + dnsConfig *runtime.DNSConfig + hostname string + ipcMode runtime.NamespaceMode + expectedCalls []ostesting.CalledDetail + }{ + "should check host /dev/shm existence when ipc mode is NODE": { + ipcMode: runtime.NamespaceMode_NODE, + expectedCalls: []ostesting.CalledDetail{ + { + Name: "Hostname", + }, + { + Name: "WriteFile", + Arguments: []interface{}{ + filepath.Join(testRootDir, sandboxesDir, testID, "hostname"), + []byte(realhostname + "\n"), + os.FileMode(0644), + }, + }, + { + Name: "CopyFile", + Arguments: []interface{}{ + "/etc/hosts", + filepath.Join(testRootDir, sandboxesDir, testID, "hosts"), + os.FileMode(0644), + }, + }, + { + Name: "CopyFile", + Arguments: []interface{}{ + "/etc/resolv.conf", + filepath.Join(testRootDir, sandboxesDir, testID, "resolv.conf"), + os.FileMode(0644), + }, + }, + { + Name: "Stat", + Arguments: []interface{}{"/dev/shm"}, + }, + }, + }, + "should create new /etc/resolv.conf if DNSOptions is set": { + dnsConfig: &runtime.DNSConfig{ + Servers: []string{"8.8.8.8"}, + Searches: []string{"114.114.114.114"}, + Options: []string{"timeout:1"}, + }, + ipcMode: runtime.NamespaceMode_NODE, + expectedCalls: []ostesting.CalledDetail{ + { + Name: "Hostname", + }, + { + Name: "WriteFile", + Arguments: []interface{}{ + filepath.Join(testRootDir, sandboxesDir, testID, "hostname"), + []byte(realhostname + "\n"), + os.FileMode(0644), + }, + }, + { + Name: "CopyFile", + Arguments: []interface{}{ + "/etc/hosts", + filepath.Join(testRootDir, sandboxesDir, testID, "hosts"), + os.FileMode(0644), + }, + }, + { + Name: "WriteFile", + Arguments: []interface{}{ + filepath.Join(testRootDir, sandboxesDir, testID, "resolv.conf"), + []byte(`search 114.114.114.114 +nameserver 8.8.8.8 +options timeout:1 +`), os.FileMode(0644), + }, + }, + { + Name: "Stat", + Arguments: []interface{}{"/dev/shm"}, + }, + }, + }, + "should create sandbox shm when ipc namespace mode is not NODE": { + ipcMode: runtime.NamespaceMode_POD, + expectedCalls: []ostesting.CalledDetail{ + { + Name: "Hostname", + }, + { + Name: "WriteFile", + Arguments: []interface{}{ + filepath.Join(testRootDir, sandboxesDir, testID, "hostname"), + []byte(realhostname + "\n"), + os.FileMode(0644), + }, + }, + { + Name: "CopyFile", + Arguments: []interface{}{ + "/etc/hosts", + filepath.Join(testRootDir, sandboxesDir, testID, "hosts"), + os.FileMode(0644), + }, + }, + { + Name: "CopyFile", + Arguments: []interface{}{ + "/etc/resolv.conf", + filepath.Join(testRootDir, sandboxesDir, testID, "resolv.conf"), + os.FileMode(0644), + }, + }, + { + Name: "MkdirAll", + Arguments: []interface{}{ + filepath.Join(testStateDir, sandboxesDir, testID, "shm"), + os.FileMode(0700), + }, + }, + { + Name: "Mount", + // Ignore arguments which are too complex to check. + }, + }, + }, + "should create /etc/hostname when hostname is set": { + hostname: "test-hostname", + ipcMode: runtime.NamespaceMode_NODE, + expectedCalls: []ostesting.CalledDetail{ + { + Name: "WriteFile", + Arguments: []interface{}{ + filepath.Join(testRootDir, sandboxesDir, testID, "hostname"), + []byte("test-hostname\n"), + os.FileMode(0644), + }, + }, + { + Name: "CopyFile", + Arguments: []interface{}{ + "/etc/hosts", + filepath.Join(testRootDir, sandboxesDir, testID, "hosts"), + os.FileMode(0644), + }, + }, + { + Name: "CopyFile", + Arguments: []interface{}{ + "/etc/resolv.conf", + filepath.Join(testRootDir, sandboxesDir, testID, "resolv.conf"), + os.FileMode(0644), + }, + }, + { + Name: "Stat", + Arguments: []interface{}{"/dev/shm"}, + }, + }, + }, + } { + t.Logf("TestCase %q", desc) + c := newTestCRIService() + c.os.(*ostesting.FakeOS).HostnameFn = func() (string, error) { + return realhostname, nil + } + cfg := &runtime.PodSandboxConfig{ + Hostname: test.hostname, + DnsConfig: test.dnsConfig, + Linux: &runtime.LinuxPodSandboxConfig{ + SecurityContext: &runtime.LinuxSandboxSecurityContext{ + NamespaceOptions: &runtime.NamespaceOption{ + Ipc: test.ipcMode, + }, + }, + }, + } + c.setupSandboxFiles(testID, cfg) + calls := c.os.(*ostesting.FakeOS).GetCalls() + assert.Len(t, calls, len(test.expectedCalls)) + for i, expected := range test.expectedCalls { + if expected.Arguments == nil { + // Ignore arguments. + expected.Arguments = calls[i].Arguments + } + assert.Equal(t, expected, calls[i]) + } + } +} + +func TestParseDNSOption(t *testing.T) { + for desc, test := range map[string]struct { + servers []string + searches []string + options []string + expectedContent string + expectErr bool + }{ + "empty dns options should return empty content": {}, + "non-empty dns options should return correct content": { + servers: []string{"8.8.8.8", "server.google.com"}, + searches: []string{"114.114.114.114"}, + options: []string{"timeout:1"}, + expectedContent: `search 114.114.114.114 +nameserver 8.8.8.8 +nameserver server.google.com +options timeout:1 +`, + }, + "should return error if dns search exceeds limit(6)": { + searches: []string{ + "server0.google.com", + "server1.google.com", + "server2.google.com", + "server3.google.com", + "server4.google.com", + "server5.google.com", + "server6.google.com", + }, + expectErr: true, + }, + } { + t.Logf("TestCase %q", desc) + resolvContent, err := parseDNSOptions(test.servers, test.searches, test.options) + if test.expectErr { + assert.Error(t, err) + continue + } + assert.NoError(t, err) + assert.Equal(t, resolvContent, test.expectedContent) + } +} + +func TestSandboxDisableCgroup(t *testing.T) { + config, imageConfig, _ := getRunPodSandboxTestData() + c := newTestCRIService() + c.config.DisableCgroup = true + spec, err := c.sandboxContainerSpec("test-id", config, imageConfig, "test-cni", []string{}) + require.NoError(t, err) + + t.Log("resource limit should not be set") + assert.Nil(t, spec.Linux.Resources.Memory) + assert.Nil(t, spec.Linux.Resources.CPU) + + t.Log("cgroup path should be empty") + assert.Empty(t, spec.Linux.CgroupsPath) +} + +// TODO(random-liu): [P1] Add unit test for different error cases to make sure +// the function cleans up on error properly. diff --git a/pkg/server/sandbox_run_other.go b/pkg/server/sandbox_run_other.go new file mode 100644 index 000000000..61d3904f7 --- /dev/null +++ b/pkg/server/sandbox_run_other.go @@ -0,0 +1,55 @@ +// +build !windows,!linux + +/* + Copyright The containerd 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 + + http://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. +*/ + +package server + +import ( + "github.com/containerd/containerd" + "github.com/containerd/containerd/oci" + imagespec "github.com/opencontainers/image-spec/specs-go/v1" + runtimespec "github.com/opencontainers/runtime-spec/specs-go" + runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" +) + +func (c *criService) sandboxContainerSpec(id string, config *runtime.PodSandboxConfig, + imageConfig *imagespec.ImageConfig, nsPath string, runtimePodAnnotations []string) (_ *runtimespec.Spec, retErr error) { + return c.runtimeSpec(id, "") +} + +// sandboxContainerSpecOpts generates OCI spec options for +// the sandbox container. +func (c *criService) sandboxContainerSpecOpts(config *runtime.PodSandboxConfig, imageConfig *imagespec.ImageConfig) ([]oci.SpecOpts, error) { + return []oci.SpecOpts{}, nil +} + +// setupSandboxFiles sets up necessary sandbox files including /dev/shm, /etc/hosts, +// /etc/resolv.conf and /etc/hostname. +func (c *criService) setupSandboxFiles(id string, config *runtime.PodSandboxConfig) error { + return nil +} + +// cleanupSandboxFiles unmount some sandbox files, we rely on the removal of sandbox root directory to +// remove these files. Unmount should *NOT* return error if the mount point is already unmounted. +func (c *criService) cleanupSandboxFiles(id string, config *runtime.PodSandboxConfig) error { + return nil +} + +// taskOpts generates task options for a (sandbox) container. +func (c *criService) taskOpts(runtimeType string) []containerd.NewTaskOpts { + return []containerd.NewTaskOpts{} +} diff --git a/pkg/server/sandbox_run_other_test.go b/pkg/server/sandbox_run_other_test.go new file mode 100644 index 000000000..daf903908 --- /dev/null +++ b/pkg/server/sandbox_run_other_test.go @@ -0,0 +1,35 @@ +// +build !windows,!linux + +/* + Copyright The containerd 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 + + http://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. +*/ + +package server + +import ( + "testing" + + imagespec "github.com/opencontainers/image-spec/specs-go/v1" + runtimespec "github.com/opencontainers/runtime-spec/specs-go" + runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" +) + +func getRunPodSandboxTestData() (*runtime.PodSandboxConfig, *imagespec.ImageConfig, func(*testing.T, string, *runtimespec.Spec)) { + config := &runtime.PodSandboxConfig{} + imageConfig := &imagespec.ImageConfig{} + specCheck := func(t *testing.T, id string, spec *runtimespec.Spec) { + } + return config, imageConfig, specCheck +} diff --git a/pkg/server/sandbox_run_test.go b/pkg/server/sandbox_run_test.go new file mode 100644 index 000000000..cb06b8cba --- /dev/null +++ b/pkg/server/sandbox_run_test.go @@ -0,0 +1,500 @@ +/* + Copyright The containerd 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 + + http://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. +*/ + +package server + +import ( + "net" + "testing" + + cni "github.com/containerd/go-cni" + "github.com/containerd/typeurl" + imagespec "github.com/opencontainers/image-spec/specs-go/v1" + runtimespec "github.com/opencontainers/runtime-spec/specs-go" + "github.com/stretchr/testify/assert" + runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" + + "github.com/containerd/containerd/pkg/annotations" + criconfig "github.com/containerd/containerd/pkg/config" + sandboxstore "github.com/containerd/containerd/pkg/store/sandbox" +) + +func TestSandboxContainerSpec(t *testing.T) { + testID := "test-id" + nsPath := "test-cni" + for desc, test := range map[string]struct { + configChange func(*runtime.PodSandboxConfig) + podAnnotations []string + imageConfigChange func(*imagespec.ImageConfig) + specCheck func(*testing.T, *runtimespec.Spec) + expectErr bool + }{ + "should return error when entrypoint and cmd are empty": { + imageConfigChange: func(c *imagespec.ImageConfig) { + c.Entrypoint = nil + c.Cmd = nil + }, + expectErr: true, + }, + "a passthrough annotation should be passed as an OCI annotation": { + podAnnotations: []string{"c"}, + specCheck: func(t *testing.T, spec *runtimespec.Spec) { + assert.Equal(t, spec.Annotations["c"], "d") + }, + }, + "a non-passthrough annotation should not be passed as an OCI annotation": { + configChange: func(c *runtime.PodSandboxConfig) { + c.Annotations["d"] = "e" + }, + podAnnotations: []string{"c"}, + specCheck: func(t *testing.T, spec *runtimespec.Spec) { + assert.Equal(t, spec.Annotations["c"], "d") + _, ok := spec.Annotations["d"] + assert.False(t, ok) + }, + }, + "passthrough annotations should support wildcard match": { + configChange: func(c *runtime.PodSandboxConfig) { + c.Annotations["t.f"] = "j" + c.Annotations["z.g"] = "o" + c.Annotations["z"] = "o" + c.Annotations["y.ca"] = "b" + c.Annotations["y"] = "b" + }, + podAnnotations: []string{"t*", "z.*", "y.c*"}, + specCheck: func(t *testing.T, spec *runtimespec.Spec) { + assert.Equal(t, spec.Annotations["t.f"], "j") + assert.Equal(t, spec.Annotations["z.g"], "o") + assert.Equal(t, spec.Annotations["y.ca"], "b") + _, ok := spec.Annotations["y"] + assert.False(t, ok) + _, ok = spec.Annotations["z"] + assert.False(t, ok) + }, + }, + } { + t.Logf("TestCase %q", desc) + c := newTestCRIService() + config, imageConfig, specCheck := getRunPodSandboxTestData() + if test.configChange != nil { + test.configChange(config) + } + + if test.imageConfigChange != nil { + test.imageConfigChange(imageConfig) + } + spec, err := c.sandboxContainerSpec(testID, config, imageConfig, nsPath, + test.podAnnotations) + if test.expectErr { + assert.Error(t, err) + assert.Nil(t, spec) + continue + } + assert.NoError(t, err) + assert.NotNil(t, spec) + specCheck(t, testID, spec) + if test.specCheck != nil { + test.specCheck(t, spec) + } + } +} + +func TestTypeurlMarshalUnmarshalSandboxMeta(t *testing.T) { + for desc, test := range map[string]struct { + configChange func(*runtime.PodSandboxConfig) + }{ + "should marshal original config": {}, + "should marshal Linux": { + configChange: func(c *runtime.PodSandboxConfig) { + if c.Linux == nil { + c.Linux = &runtime.LinuxPodSandboxConfig{} + } + c.Linux.SecurityContext = &runtime.LinuxSandboxSecurityContext{ + NamespaceOptions: &runtime.NamespaceOption{ + Network: runtime.NamespaceMode_NODE, + Pid: runtime.NamespaceMode_NODE, + Ipc: runtime.NamespaceMode_NODE, + }, + SupplementalGroups: []int64{1111, 2222}, + } + }, + }, + } { + t.Logf("TestCase %q", desc) + meta := &sandboxstore.Metadata{ + ID: "1", + Name: "sandbox_1", + NetNSPath: "/home/cloud", + } + meta.Config, _, _ = getRunPodSandboxTestData() + if test.configChange != nil { + test.configChange(meta.Config) + } + + any, err := typeurl.MarshalAny(meta) + assert.NoError(t, err) + data, err := typeurl.UnmarshalAny(any) + assert.NoError(t, err) + assert.IsType(t, &sandboxstore.Metadata{}, data) + curMeta, ok := data.(*sandboxstore.Metadata) + assert.True(t, ok) + assert.Equal(t, meta, curMeta) + } +} + +func TestToCNIPortMappings(t *testing.T) { + for desc, test := range map[string]struct { + criPortMappings []*runtime.PortMapping + cniPortMappings []cni.PortMapping + }{ + "empty CRI port mapping should map to empty CNI port mapping": {}, + "CRI port mapping should be converted to CNI port mapping properly": { + criPortMappings: []*runtime.PortMapping{ + { + Protocol: runtime.Protocol_UDP, + ContainerPort: 1234, + HostPort: 5678, + HostIp: "123.124.125.126", + }, + { + Protocol: runtime.Protocol_TCP, + ContainerPort: 4321, + HostPort: 8765, + HostIp: "126.125.124.123", + }, + { + Protocol: runtime.Protocol_SCTP, + ContainerPort: 1234, + HostPort: 5678, + HostIp: "123.124.125.126", + }, + }, + cniPortMappings: []cni.PortMapping{ + { + HostPort: 5678, + ContainerPort: 1234, + Protocol: "udp", + HostIP: "123.124.125.126", + }, + { + HostPort: 8765, + ContainerPort: 4321, + Protocol: "tcp", + HostIP: "126.125.124.123", + }, + { + HostPort: 5678, + ContainerPort: 1234, + Protocol: "sctp", + HostIP: "123.124.125.126", + }, + }, + }, + "CRI port mapping without host port should be skipped": { + criPortMappings: []*runtime.PortMapping{ + { + Protocol: runtime.Protocol_UDP, + ContainerPort: 1234, + HostIp: "123.124.125.126", + }, + { + Protocol: runtime.Protocol_TCP, + ContainerPort: 4321, + HostPort: 8765, + HostIp: "126.125.124.123", + }, + }, + cniPortMappings: []cni.PortMapping{ + { + HostPort: 8765, + ContainerPort: 4321, + Protocol: "tcp", + HostIP: "126.125.124.123", + }, + }, + }, + "CRI port mapping with unsupported protocol should be skipped": { + criPortMappings: []*runtime.PortMapping{ + { + Protocol: runtime.Protocol_TCP, + ContainerPort: 4321, + HostPort: 8765, + HostIp: "126.125.124.123", + }, + }, + cniPortMappings: []cni.PortMapping{ + { + HostPort: 8765, + ContainerPort: 4321, + Protocol: "tcp", + HostIP: "126.125.124.123", + }, + }, + }, + } { + t.Logf("TestCase %q", desc) + assert.Equal(t, test.cniPortMappings, toCNIPortMappings(test.criPortMappings)) + } +} + +func TestSelectPodIP(t *testing.T) { + for desc, test := range map[string]struct { + ips []string + expectedIP string + expectedAdditionalIPs []string + }{ + "ipv4 should be picked even if ipv6 comes first": { + ips: []string{"2001:db8:85a3::8a2e:370:7334", "192.168.17.43"}, + expectedIP: "192.168.17.43", + expectedAdditionalIPs: []string{"2001:db8:85a3::8a2e:370:7334"}, + }, + "ipv4 should be picked when there is only ipv4": { + ips: []string{"192.168.17.43"}, + expectedIP: "192.168.17.43", + expectedAdditionalIPs: nil, + }, + "ipv6 should be picked when there is no ipv4": { + ips: []string{"2001:db8:85a3::8a2e:370:7334"}, + expectedIP: "2001:db8:85a3::8a2e:370:7334", + expectedAdditionalIPs: nil, + }, + "the first ipv4 should be picked when there are multiple ipv4": { // unlikely to happen + ips: []string{"2001:db8:85a3::8a2e:370:7334", "192.168.17.43", "2001:db8:85a3::8a2e:370:7335", "192.168.17.45"}, + expectedIP: "192.168.17.43", + expectedAdditionalIPs: []string{"2001:db8:85a3::8a2e:370:7334", "2001:db8:85a3::8a2e:370:7335", "192.168.17.45"}, + }, + } { + t.Logf("TestCase %q", desc) + var ipConfigs []*cni.IPConfig + for _, ip := range test.ips { + ipConfigs = append(ipConfigs, &cni.IPConfig{ + IP: net.ParseIP(ip), + }) + } + ip, additionalIPs := selectPodIPs(ipConfigs) + assert.Equal(t, test.expectedIP, ip) + assert.Equal(t, test.expectedAdditionalIPs, additionalIPs) + } +} + +func TestHostAccessingSandbox(t *testing.T) { + privilegedContext := &runtime.PodSandboxConfig{ + Linux: &runtime.LinuxPodSandboxConfig{ + SecurityContext: &runtime.LinuxSandboxSecurityContext{ + Privileged: true, + }, + }, + } + nonPrivilegedContext := &runtime.PodSandboxConfig{ + Linux: &runtime.LinuxPodSandboxConfig{ + SecurityContext: &runtime.LinuxSandboxSecurityContext{ + Privileged: false, + }, + }, + } + hostNamespace := &runtime.PodSandboxConfig{ + Linux: &runtime.LinuxPodSandboxConfig{ + SecurityContext: &runtime.LinuxSandboxSecurityContext{ + Privileged: false, + NamespaceOptions: &runtime.NamespaceOption{ + Network: runtime.NamespaceMode_NODE, + Pid: runtime.NamespaceMode_NODE, + Ipc: runtime.NamespaceMode_NODE, + }, + }, + }, + } + tests := []struct { + name string + config *runtime.PodSandboxConfig + want bool + }{ + {"Security Context is nil", nil, false}, + {"Security Context is privileged", privilegedContext, false}, + {"Security Context is not privileged", nonPrivilegedContext, false}, + {"Security Context namespace host access", hostNamespace, true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := hostAccessingSandbox(tt.config); got != tt.want { + t.Errorf("hostAccessingSandbox() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestGetSandboxRuntime(t *testing.T) { + untrustedWorkloadRuntime := criconfig.Runtime{ + Type: "io.containerd.runtime.v1.linux", + Engine: "untrusted-workload-runtime", + Root: "", + } + + defaultRuntime := criconfig.Runtime{ + Type: "io.containerd.runtime.v1.linux", + Engine: "default-runtime", + Root: "", + } + + fooRuntime := criconfig.Runtime{ + Type: "io.containerd.runtime.v1.linux", + Engine: "foo-bar", + Root: "", + } + + for desc, test := range map[string]struct { + sandboxConfig *runtime.PodSandboxConfig + runtimeHandler string + runtimes map[string]criconfig.Runtime + expectErr bool + expectedRuntime criconfig.Runtime + }{ + "should return error if untrusted workload requires host access": { + sandboxConfig: &runtime.PodSandboxConfig{ + Linux: &runtime.LinuxPodSandboxConfig{ + SecurityContext: &runtime.LinuxSandboxSecurityContext{ + Privileged: false, + NamespaceOptions: &runtime.NamespaceOption{ + Network: runtime.NamespaceMode_NODE, + Pid: runtime.NamespaceMode_NODE, + Ipc: runtime.NamespaceMode_NODE, + }, + }, + }, + Annotations: map[string]string{ + annotations.UntrustedWorkload: "true", + }, + }, + runtimes: map[string]criconfig.Runtime{ + criconfig.RuntimeDefault: defaultRuntime, + criconfig.RuntimeUntrusted: untrustedWorkloadRuntime, + }, + expectErr: true, + }, + "should use untrusted workload runtime for untrusted workload": { + sandboxConfig: &runtime.PodSandboxConfig{ + Annotations: map[string]string{ + annotations.UntrustedWorkload: "true", + }, + }, + runtimes: map[string]criconfig.Runtime{ + criconfig.RuntimeDefault: defaultRuntime, + criconfig.RuntimeUntrusted: untrustedWorkloadRuntime, + }, + expectedRuntime: untrustedWorkloadRuntime, + }, + "should use default runtime for regular workload": { + sandboxConfig: &runtime.PodSandboxConfig{}, + runtimes: map[string]criconfig.Runtime{ + criconfig.RuntimeDefault: defaultRuntime, + }, + expectedRuntime: defaultRuntime, + }, + "should use default runtime for trusted workload": { + sandboxConfig: &runtime.PodSandboxConfig{ + Annotations: map[string]string{ + annotations.UntrustedWorkload: "false", + }, + }, + runtimes: map[string]criconfig.Runtime{ + criconfig.RuntimeDefault: defaultRuntime, + criconfig.RuntimeUntrusted: untrustedWorkloadRuntime, + }, + expectedRuntime: defaultRuntime, + }, + "should return error if untrusted workload runtime is required but not configured": { + sandboxConfig: &runtime.PodSandboxConfig{ + Annotations: map[string]string{ + annotations.UntrustedWorkload: "true", + }, + }, + runtimes: map[string]criconfig.Runtime{ + criconfig.RuntimeDefault: defaultRuntime, + }, + expectErr: true, + }, + "should use 'untrusted' runtime for untrusted workload": { + sandboxConfig: &runtime.PodSandboxConfig{ + Annotations: map[string]string{ + annotations.UntrustedWorkload: "true", + }, + }, + runtimes: map[string]criconfig.Runtime{ + criconfig.RuntimeDefault: defaultRuntime, + criconfig.RuntimeUntrusted: untrustedWorkloadRuntime, + }, + expectedRuntime: untrustedWorkloadRuntime, + }, + "should use 'untrusted' runtime for untrusted workload & handler": { + sandboxConfig: &runtime.PodSandboxConfig{ + Annotations: map[string]string{ + annotations.UntrustedWorkload: "true", + }, + }, + runtimeHandler: "untrusted", + runtimes: map[string]criconfig.Runtime{ + criconfig.RuntimeDefault: defaultRuntime, + criconfig.RuntimeUntrusted: untrustedWorkloadRuntime, + }, + expectedRuntime: untrustedWorkloadRuntime, + }, + "should return an error if untrusted annotation with conflicting handler": { + sandboxConfig: &runtime.PodSandboxConfig{ + Annotations: map[string]string{ + annotations.UntrustedWorkload: "true", + }, + }, + runtimeHandler: "foo", + runtimes: map[string]criconfig.Runtime{ + criconfig.RuntimeDefault: defaultRuntime, + criconfig.RuntimeUntrusted: untrustedWorkloadRuntime, + "foo": fooRuntime, + }, + expectErr: true, + }, + "should use correct runtime for a runtime handler": { + sandboxConfig: &runtime.PodSandboxConfig{}, + runtimeHandler: "foo", + runtimes: map[string]criconfig.Runtime{ + criconfig.RuntimeDefault: defaultRuntime, + criconfig.RuntimeUntrusted: untrustedWorkloadRuntime, + "foo": fooRuntime, + }, + expectedRuntime: fooRuntime, + }, + "should return error if runtime handler is required but not configured": { + sandboxConfig: &runtime.PodSandboxConfig{}, + runtimeHandler: "bar", + runtimes: map[string]criconfig.Runtime{ + criconfig.RuntimeDefault: defaultRuntime, + "foo": fooRuntime, + }, + expectErr: true, + }, + } { + t.Run(desc, func(t *testing.T) { + cri := newTestCRIService() + cri.config = criconfig.Config{ + PluginConfig: criconfig.DefaultConfig(), + } + cri.config.ContainerdConfig.DefaultRuntimeName = criconfig.RuntimeDefault + cri.config.ContainerdConfig.Runtimes = test.runtimes + r, err := cri.getSandboxRuntime(test.sandboxConfig, test.runtimeHandler) + assert.Equal(t, test.expectErr, err != nil) + assert.Equal(t, test.expectedRuntime, r) + }) + } +} diff --git a/vendor/github.com/containerd/cri/pkg/server/sandbox_run_windows.go b/pkg/server/sandbox_run_windows.go similarity index 96% rename from vendor/github.com/containerd/cri/pkg/server/sandbox_run_windows.go rename to pkg/server/sandbox_run_windows.go index 85105c299..27404de25 100644 --- a/vendor/github.com/containerd/cri/pkg/server/sandbox_run_windows.go +++ b/pkg/server/sandbox_run_windows.go @@ -26,8 +26,8 @@ import ( "github.com/pkg/errors" runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" - "github.com/containerd/cri/pkg/annotations" - customopts "github.com/containerd/cri/pkg/containerd/opts" + "github.com/containerd/containerd/pkg/annotations" + customopts "github.com/containerd/containerd/pkg/containerd/opts" ) func (c *criService) sandboxContainerSpec(id string, config *runtime.PodSandboxConfig, diff --git a/pkg/server/sandbox_run_windows_test.go b/pkg/server/sandbox_run_windows_test.go new file mode 100644 index 000000000..7061fa431 --- /dev/null +++ b/pkg/server/sandbox_run_windows_test.go @@ -0,0 +1,86 @@ +// +build windows + +/* + Copyright The containerd 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 + + http://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. +*/ + +package server + +import ( + "testing" + + imagespec "github.com/opencontainers/image-spec/specs-go/v1" + runtimespec "github.com/opencontainers/runtime-spec/specs-go" + "github.com/stretchr/testify/assert" + runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" + + "github.com/containerd/containerd/pkg/annotations" + "github.com/containerd/containerd/pkg/containerd/opts" +) + +func getRunPodSandboxTestData() (*runtime.PodSandboxConfig, *imagespec.ImageConfig, func(*testing.T, string, *runtimespec.Spec)) { + config := &runtime.PodSandboxConfig{ + Metadata: &runtime.PodSandboxMetadata{ + Name: "test-name", + Uid: "test-uid", + Namespace: "test-ns", + Attempt: 1, + }, + Hostname: "test-hostname", + LogDirectory: "test-log-directory", + Labels: map[string]string{"a": "b"}, + Annotations: map[string]string{"c": "d"}, + } + imageConfig := &imagespec.ImageConfig{ + Env: []string{"a=b", "c=d"}, + Entrypoint: []string{"/pause"}, + Cmd: []string{"forever"}, + WorkingDir: "/workspace", + } + specCheck := func(t *testing.T, id string, spec *runtimespec.Spec) { + assert.Equal(t, "test-hostname", spec.Hostname) + assert.Nil(t, spec.Root) + assert.Contains(t, spec.Process.Env, "a=b", "c=d") + assert.Equal(t, []string{"/pause", "forever"}, spec.Process.Args) + assert.Equal(t, "/workspace", spec.Process.Cwd) + assert.EqualValues(t, *spec.Windows.Resources.CPU.Shares, opts.DefaultSandboxCPUshares) + + t.Logf("Check PodSandbox annotations") + assert.Contains(t, spec.Annotations, annotations.SandboxID) + assert.EqualValues(t, spec.Annotations[annotations.SandboxID], id) + + assert.Contains(t, spec.Annotations, annotations.ContainerType) + assert.EqualValues(t, spec.Annotations[annotations.ContainerType], annotations.ContainerTypeSandbox) + + assert.Contains(t, spec.Annotations, annotations.SandboxLogDir) + assert.EqualValues(t, spec.Annotations[annotations.SandboxLogDir], "test-log-directory") + } + return config, imageConfig, specCheck +} + +func TestSandboxWindowsNetworkNamespace(t *testing.T) { + testID := "test-id" + nsPath := "test-cni" + c := newTestCRIService() + + config, imageConfig, specCheck := getRunPodSandboxTestData() + spec, err := c.sandboxContainerSpec(testID, config, imageConfig, nsPath, nil) + assert.NoError(t, err) + assert.NotNil(t, spec) + specCheck(t, testID, spec) + assert.NotNil(t, spec.Windows) + assert.NotNil(t, spec.Windows.Network) + assert.Equal(t, nsPath, spec.Windows.Network.NetworkNamespace) +} diff --git a/vendor/github.com/containerd/cri/pkg/server/sandbox_status.go b/pkg/server/sandbox_status.go similarity index 99% rename from vendor/github.com/containerd/cri/pkg/server/sandbox_status.go rename to pkg/server/sandbox_status.go index 5644ab1be..66b4919bf 100644 --- a/vendor/github.com/containerd/cri/pkg/server/sandbox_status.go +++ b/pkg/server/sandbox_status.go @@ -28,7 +28,7 @@ import ( "golang.org/x/net/context" runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" - sandboxstore "github.com/containerd/cri/pkg/store/sandbox" + sandboxstore "github.com/containerd/containerd/pkg/store/sandbox" ) // PodSandboxStatus returns the status of the PodSandbox. diff --git a/pkg/server/sandbox_status_test.go b/pkg/server/sandbox_status_test.go new file mode 100644 index 000000000..d09d253e6 --- /dev/null +++ b/pkg/server/sandbox_status_test.go @@ -0,0 +1,116 @@ +/* + Copyright The containerd 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 + + http://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. +*/ + +package server + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" + + sandboxstore "github.com/containerd/containerd/pkg/store/sandbox" +) + +func TestPodSandboxStatus(t *testing.T) { + const ( + id = "test-id" + ip = "10.10.10.10" + ) + additionalIPs := []string{"8.8.8.8", "2001:db8:85a3::8a2e:370:7334"} + createdAt := time.Now() + config := &runtime.PodSandboxConfig{ + Metadata: &runtime.PodSandboxMetadata{ + Name: "test-name", + Uid: "test-uid", + Namespace: "test-ns", + Attempt: 1, + }, + Linux: &runtime.LinuxPodSandboxConfig{ + SecurityContext: &runtime.LinuxSandboxSecurityContext{ + NamespaceOptions: &runtime.NamespaceOption{ + Network: runtime.NamespaceMode_NODE, + Pid: runtime.NamespaceMode_CONTAINER, + Ipc: runtime.NamespaceMode_POD, + }, + }, + }, + Labels: map[string]string{"a": "b"}, + Annotations: map[string]string{"c": "d"}, + } + metadata := sandboxstore.Metadata{ + ID: id, + Name: "test-name", + Config: config, + RuntimeHandler: "test-runtime-handler", + } + + expected := &runtime.PodSandboxStatus{ + Id: id, + Metadata: config.GetMetadata(), + CreatedAt: createdAt.UnixNano(), + Network: &runtime.PodSandboxNetworkStatus{ + Ip: ip, + AdditionalIps: []*runtime.PodIP{ + { + Ip: additionalIPs[0], + }, + { + Ip: additionalIPs[1], + }, + }, + }, + Linux: &runtime.LinuxPodSandboxStatus{ + Namespaces: &runtime.Namespace{ + Options: &runtime.NamespaceOption{ + Network: runtime.NamespaceMode_NODE, + Pid: runtime.NamespaceMode_CONTAINER, + Ipc: runtime.NamespaceMode_POD, + }, + }, + }, + Labels: config.GetLabels(), + Annotations: config.GetAnnotations(), + RuntimeHandler: "test-runtime-handler", + } + for desc, test := range map[string]struct { + state sandboxstore.State + expectedState runtime.PodSandboxState + }{ + "sandbox state ready": { + state: sandboxstore.StateReady, + expectedState: runtime.PodSandboxState_SANDBOX_READY, + }, + "sandbox state not ready": { + state: sandboxstore.StateNotReady, + expectedState: runtime.PodSandboxState_SANDBOX_NOTREADY, + }, + "sandbox state unknown": { + state: sandboxstore.StateUnknown, + expectedState: runtime.PodSandboxState_SANDBOX_NOTREADY, + }, + } { + t.Logf("TestCase: %s", desc) + status := sandboxstore.Status{ + CreatedAt: createdAt, + State: test.state, + } + expected.State = test.expectedState + got := toCRISandboxStatus(metadata, status, ip, additionalIPs) + assert.Equal(t, expected, got) + } +} diff --git a/vendor/github.com/containerd/cri/pkg/server/sandbox_stop.go b/pkg/server/sandbox_stop.go similarity index 98% rename from vendor/github.com/containerd/cri/pkg/server/sandbox_stop.go rename to pkg/server/sandbox_stop.go index 9b6e0a6ec..a931505e5 100644 --- a/vendor/github.com/containerd/cri/pkg/server/sandbox_stop.go +++ b/pkg/server/sandbox_stop.go @@ -27,8 +27,8 @@ import ( "golang.org/x/net/context" runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" - ctrdutil "github.com/containerd/cri/pkg/containerd/util" - sandboxstore "github.com/containerd/cri/pkg/store/sandbox" + ctrdutil "github.com/containerd/containerd/pkg/containerd/util" + sandboxstore "github.com/containerd/containerd/pkg/store/sandbox" ) // StopPodSandbox stops the sandbox. If there are any running containers in the diff --git a/pkg/server/sandbox_stop_test.go b/pkg/server/sandbox_stop_test.go new file mode 100644 index 000000000..53d8ca477 --- /dev/null +++ b/pkg/server/sandbox_stop_test.go @@ -0,0 +1,73 @@ +/* + Copyright The containerd 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 + + http://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. +*/ + +package server + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "golang.org/x/net/context" + + sandboxstore "github.com/containerd/containerd/pkg/store/sandbox" +) + +func TestWaitSandboxStop(t *testing.T) { + id := "test-id" + for desc, test := range map[string]struct { + state sandboxstore.State + cancel bool + timeout time.Duration + expectErr bool + }{ + "should return error if timeout exceeds": { + state: sandboxstore.StateReady, + timeout: 200 * time.Millisecond, + expectErr: true, + }, + "should return error if context is cancelled": { + state: sandboxstore.StateReady, + timeout: time.Hour, + cancel: true, + expectErr: true, + }, + "should not return error if sandbox is stopped before timeout": { + state: sandboxstore.StateNotReady, + timeout: time.Hour, + expectErr: false, + }, + } { + c := newTestCRIService() + sandbox := sandboxstore.NewSandbox( + sandboxstore.Metadata{ID: id}, + sandboxstore.Status{State: test.state}, + ) + ctx := context.Background() + if test.cancel { + cancelledCtx, cancel := context.WithCancel(ctx) + cancel() + ctx = cancelledCtx + } + if test.timeout > 0 { + timeoutCtx, cancel := context.WithTimeout(ctx, test.timeout) + defer cancel() + ctx = timeoutCtx + } + err := c.waitSandboxStop(ctx, sandbox) + assert.Equal(t, test.expectErr, err != nil, desc) + } +} diff --git a/vendor/github.com/containerd/cri/pkg/server/service.go b/pkg/server/service.go similarity index 94% rename from vendor/github.com/containerd/cri/pkg/server/service.go rename to pkg/server/service.go index 94e02591a..bd0b480bb 100644 --- a/vendor/github.com/containerd/cri/pkg/server/service.go +++ b/pkg/server/service.go @@ -27,25 +27,25 @@ import ( "github.com/containerd/containerd" "github.com/containerd/containerd/oci" + "github.com/containerd/containerd/pkg/streaming" "github.com/containerd/containerd/plugin" - "github.com/containerd/cri/pkg/streaming" cni "github.com/containerd/go-cni" "github.com/pkg/errors" "github.com/sirupsen/logrus" "google.golang.org/grpc" runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" - "github.com/containerd/cri/pkg/store/label" + "github.com/containerd/containerd/pkg/store/label" - "github.com/containerd/cri/pkg/atomic" - criconfig "github.com/containerd/cri/pkg/config" - ctrdutil "github.com/containerd/cri/pkg/containerd/util" - osinterface "github.com/containerd/cri/pkg/os" - "github.com/containerd/cri/pkg/registrar" - containerstore "github.com/containerd/cri/pkg/store/container" - imagestore "github.com/containerd/cri/pkg/store/image" - sandboxstore "github.com/containerd/cri/pkg/store/sandbox" - snapshotstore "github.com/containerd/cri/pkg/store/snapshot" + "github.com/containerd/containerd/pkg/atomic" + criconfig "github.com/containerd/containerd/pkg/config" + ctrdutil "github.com/containerd/containerd/pkg/containerd/util" + osinterface "github.com/containerd/containerd/pkg/os" + "github.com/containerd/containerd/pkg/registrar" + containerstore "github.com/containerd/containerd/pkg/store/container" + imagestore "github.com/containerd/containerd/pkg/store/image" + sandboxstore "github.com/containerd/containerd/pkg/store/sandbox" + snapshotstore "github.com/containerd/containerd/pkg/store/snapshot" ) // grpcServices are all the grpc services provided by cri containerd. diff --git a/vendor/github.com/containerd/cri/pkg/server/service_unix.go b/pkg/server/service_linux.go similarity index 99% rename from vendor/github.com/containerd/cri/pkg/server/service_unix.go rename to pkg/server/service_linux.go index a1d9c9038..03b28f0ae 100644 --- a/vendor/github.com/containerd/cri/pkg/server/service_unix.go +++ b/pkg/server/service_linux.go @@ -1,5 +1,3 @@ -// +build !windows - /* Copyright The containerd Authors. diff --git a/pkg/server/service_other.go b/pkg/server/service_other.go new file mode 100644 index 000000000..c17f7ccae --- /dev/null +++ b/pkg/server/service_other.go @@ -0,0 +1,33 @@ +// +build !windows,!linux + +/* + Copyright The containerd 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 + + http://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. +*/ + +package server + +import ( + cni "github.com/containerd/go-cni" +) + +// initPlatform handles linux specific initialization for the CRI service. +func (c *criService) initPlatform() error { + return nil +} + +// cniLoadOptions returns cni load options for the linux. +func (c *criService) cniLoadOptions() []cni.CNIOpt { + return []cni.CNIOpt{} +} diff --git a/pkg/server/service_test.go b/pkg/server/service_test.go new file mode 100644 index 000000000..fe09b9471 --- /dev/null +++ b/pkg/server/service_test.go @@ -0,0 +1,102 @@ +/* + Copyright The containerd 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 + + http://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. +*/ + +package server + +import ( + "encoding/json" + "io/ioutil" + "os" + "testing" + + "github.com/containerd/containerd/oci" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + criconfig "github.com/containerd/containerd/pkg/config" + ostesting "github.com/containerd/containerd/pkg/os/testing" + "github.com/containerd/containerd/pkg/registrar" + servertesting "github.com/containerd/containerd/pkg/server/testing" + containerstore "github.com/containerd/containerd/pkg/store/container" + imagestore "github.com/containerd/containerd/pkg/store/image" + "github.com/containerd/containerd/pkg/store/label" + sandboxstore "github.com/containerd/containerd/pkg/store/sandbox" + snapshotstore "github.com/containerd/containerd/pkg/store/snapshot" +) + +const ( + testRootDir = "/test/root" + testStateDir = "/test/state" + // Use an image id as test sandbox image to avoid image name resolve. + // TODO(random-liu): Change this to image name after we have complete image + // management unit test framework. + testSandboxImage = "sha256:c75bebcdd211f41b3a460c7bf82970ed6c75acaab9cd4c9a4e125b03ca113798" + testImageFSPath = "/test/image/fs/path" +) + +// newTestCRIService creates a fake criService for test. +func newTestCRIService() *criService { + labels := label.NewStore() + return &criService{ + config: criconfig.Config{ + RootDir: testRootDir, + StateDir: testStateDir, + PluginConfig: criconfig.PluginConfig{ + SandboxImage: testSandboxImage, + }, + }, + imageFSPath: testImageFSPath, + os: ostesting.NewFakeOS(), + sandboxStore: sandboxstore.NewStore(labels), + imageStore: imagestore.NewStore(nil), + snapshotStore: snapshotstore.NewStore(), + sandboxNameIndex: registrar.NewRegistrar(), + containerStore: containerstore.NewStore(labels), + containerNameIndex: registrar.NewRegistrar(), + netPlugin: servertesting.NewFakeCNIPlugin(), + } +} + +func TestLoadBaseOCISpec(t *testing.T) { + spec := oci.Spec{Version: "1.0.2", Hostname: "default"} + + file, err := ioutil.TempFile("", "spec-test-") + require.NoError(t, err) + + defer func() { + assert.NoError(t, file.Close()) + assert.NoError(t, os.RemoveAll(file.Name())) + }() + + err = json.NewEncoder(file).Encode(&spec) + assert.NoError(t, err) + + config := criconfig.Config{} + config.Runtimes = map[string]criconfig.Runtime{ + "runc": {BaseRuntimeSpec: file.Name()}, + } + + specs, err := loadBaseOCISpecs(&config) + assert.NoError(t, err) + + assert.Len(t, specs, 1) + + out, ok := specs[file.Name()] + assert.True(t, ok, "expected spec with file name %q", file.Name()) + + assert.Equal(t, "1.0.2", out.Version) + assert.Equal(t, "default", out.Hostname) +} diff --git a/vendor/github.com/containerd/cri/pkg/server/service_windows.go b/pkg/server/service_windows.go similarity index 100% rename from vendor/github.com/containerd/cri/pkg/server/service_windows.go rename to pkg/server/service_windows.go diff --git a/vendor/github.com/containerd/cri/pkg/server/snapshots.go b/pkg/server/snapshots.go similarity index 96% rename from vendor/github.com/containerd/cri/pkg/server/snapshots.go rename to pkg/server/snapshots.go index 0c1670750..33d8d7ec4 100644 --- a/vendor/github.com/containerd/cri/pkg/server/snapshots.go +++ b/pkg/server/snapshots.go @@ -25,8 +25,8 @@ import ( "github.com/pkg/errors" "github.com/sirupsen/logrus" - ctrdutil "github.com/containerd/cri/pkg/containerd/util" - snapshotstore "github.com/containerd/cri/pkg/store/snapshot" + ctrdutil "github.com/containerd/containerd/pkg/containerd/util" + snapshotstore "github.com/containerd/containerd/pkg/store/snapshot" ) // snapshotsSyncer syncs snapshot stats periodically. imagefs info and container stats diff --git a/vendor/github.com/containerd/cri/pkg/server/status.go b/pkg/server/status.go similarity index 100% rename from vendor/github.com/containerd/cri/pkg/server/status.go rename to pkg/server/status.go diff --git a/vendor/github.com/containerd/cri/pkg/server/streaming.go b/pkg/server/streaming.go similarity index 98% rename from vendor/github.com/containerd/cri/pkg/server/streaming.go rename to pkg/server/streaming.go index d0089cc89..d08759cff 100644 --- a/vendor/github.com/containerd/cri/pkg/server/streaming.go +++ b/pkg/server/streaming.go @@ -32,8 +32,8 @@ import ( k8scert "k8s.io/client-go/util/cert" "k8s.io/utils/exec" - ctrdutil "github.com/containerd/cri/pkg/containerd/util" - "github.com/containerd/cri/pkg/streaming" + ctrdutil "github.com/containerd/containerd/pkg/containerd/util" + "github.com/containerd/containerd/pkg/streaming" ) type streamListenerMode int diff --git a/pkg/server/streaming_test.go b/pkg/server/streaming_test.go new file mode 100644 index 000000000..3d085796a --- /dev/null +++ b/pkg/server/streaming_test.go @@ -0,0 +1,153 @@ +/* + Copyright The containerd 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 + + http://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. +*/ + +package server + +import ( + "testing" + + "github.com/containerd/containerd/pkg/config" + "github.com/stretchr/testify/assert" +) + +func TestValidateStreamServer(t *testing.T) { + for desc, test := range map[string]struct { + *criService + tlsMode streamListenerMode + expectErr bool + }{ + "should pass with default withoutTLS": { + criService: &criService{ + config: config.Config{ + PluginConfig: config.DefaultConfig(), + }, + }, + tlsMode: withoutTLS, + expectErr: false, + }, + "should pass with x509KeyPairTLS": { + criService: &criService{ + config: config.Config{ + PluginConfig: config.PluginConfig{ + EnableTLSStreaming: true, + X509KeyPairStreaming: config.X509KeyPairStreaming{ + TLSKeyFile: "non-empty", + TLSCertFile: "non-empty", + }, + }, + }, + }, + tlsMode: x509KeyPairTLS, + expectErr: false, + }, + "should pass with selfSign": { + criService: &criService{ + config: config.Config{ + PluginConfig: config.PluginConfig{ + EnableTLSStreaming: true, + }, + }, + }, + tlsMode: selfSignTLS, + expectErr: false, + }, + "should return error with X509 keypair but not EnableTLSStreaming": { + criService: &criService{ + config: config.Config{ + PluginConfig: config.PluginConfig{ + EnableTLSStreaming: false, + X509KeyPairStreaming: config.X509KeyPairStreaming{ + TLSKeyFile: "non-empty", + TLSCertFile: "non-empty", + }, + }, + }, + }, + tlsMode: -1, + expectErr: true, + }, + "should return error with X509 TLSCertFile empty": { + criService: &criService{ + config: config.Config{ + PluginConfig: config.PluginConfig{ + EnableTLSStreaming: true, + X509KeyPairStreaming: config.X509KeyPairStreaming{ + TLSKeyFile: "non-empty", + TLSCertFile: "", + }, + }, + }, + }, + tlsMode: -1, + expectErr: true, + }, + "should return error with X509 TLSKeyFile empty": { + criService: &criService{ + config: config.Config{ + PluginConfig: config.PluginConfig{ + EnableTLSStreaming: true, + X509KeyPairStreaming: config.X509KeyPairStreaming{ + TLSKeyFile: "", + TLSCertFile: "non-empty", + }, + }, + }, + }, + tlsMode: -1, + expectErr: true, + }, + "should return error without EnableTLSStreaming and only TLSCertFile set": { + criService: &criService{ + config: config.Config{ + PluginConfig: config.PluginConfig{ + EnableTLSStreaming: false, + X509KeyPairStreaming: config.X509KeyPairStreaming{ + TLSKeyFile: "", + TLSCertFile: "non-empty", + }, + }, + }, + }, + tlsMode: -1, + expectErr: true, + }, + "should return error without EnableTLSStreaming and only TLSKeyFile set": { + criService: &criService{ + config: config.Config{ + PluginConfig: config.PluginConfig{ + EnableTLSStreaming: false, + X509KeyPairStreaming: config.X509KeyPairStreaming{ + TLSKeyFile: "non-empty", + TLSCertFile: "", + }, + }, + }, + }, + tlsMode: -1, + expectErr: true, + }, + } { + t.Run(desc, func(t *testing.T) { + tlsMode, err := getStreamListenerMode(test.criService) + if test.expectErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assert.Equal(t, test.tlsMode, tlsMode) + }) + } +} diff --git a/pkg/server/testing/fake_cni_plugin.go b/pkg/server/testing/fake_cni_plugin.go new file mode 100644 index 000000000..71a930f59 --- /dev/null +++ b/pkg/server/testing/fake_cni_plugin.go @@ -0,0 +1,59 @@ +/* + Copyright The containerd 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 + + http://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. +*/ + +package testing + +import ( + "context" + + cni "github.com/containerd/go-cni" +) + +// FakeCNIPlugin is a fake plugin used for test. +type FakeCNIPlugin struct { + StatusErr error + LoadErr error +} + +// NewFakeCNIPlugin create a FakeCNIPlugin. +func NewFakeCNIPlugin() *FakeCNIPlugin { + return &FakeCNIPlugin{} +} + +// Setup setups the network of PodSandbox. +func (f *FakeCNIPlugin) Setup(ctx context.Context, id, path string, opts ...cni.NamespaceOpts) (*cni.CNIResult, error) { + return nil, nil +} + +// Remove teardown the network of PodSandbox. +func (f *FakeCNIPlugin) Remove(ctx context.Context, id, path string, opts ...cni.NamespaceOpts) error { + return nil +} + +// Status get the status of the plugin. +func (f *FakeCNIPlugin) Status() error { + return f.StatusErr +} + +// Load loads the network config. +func (f *FakeCNIPlugin) Load(opts ...cni.CNIOpt) error { + return f.LoadErr +} + +// GetConfig returns a copy of the CNI plugin configurations as parsed by CNI +func (f *FakeCNIPlugin) GetConfig() *cni.ConfigResult { + return nil +} diff --git a/vendor/github.com/containerd/cri/pkg/server/update_runtime_config.go b/pkg/server/update_runtime_config.go similarity index 100% rename from vendor/github.com/containerd/cri/pkg/server/update_runtime_config.go rename to pkg/server/update_runtime_config.go diff --git a/pkg/server/update_runtime_config_test.go b/pkg/server/update_runtime_config_test.go new file mode 100644 index 000000000..1bb45d5bd --- /dev/null +++ b/pkg/server/update_runtime_config_test.go @@ -0,0 +1,140 @@ +/* + Copyright The containerd 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 + + http://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. +*/ + +package server + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/net/context" + runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" + + criconfig "github.com/containerd/containerd/pkg/config" + servertesting "github.com/containerd/containerd/pkg/server/testing" +) + +func TestUpdateRuntimeConfig(t *testing.T) { + const ( + testTemplate = ` +{ + "name": "test-pod-network", + "cniVersion": "0.3.1", + "plugins": [ + { + "type": "ptp", + "mtu": 1460, + "ipam": { + "type": "host-local", + "subnet": "{{.PodCIDR}}", + "ranges": [{{range $i, $range := .PodCIDRRanges}}{{if $i}}, {{end}}[{"subnet": "{{$range}}"}]{{end}}], + "routes": [{{range $i, $route := .Routes}}{{if $i}}, {{end}}{"dst": "{{$route}}"}{{end}}] + } + }, + ] +}` + testCIDR = "10.0.0.0/24, 2001:4860:4860::/64" + expected = ` +{ + "name": "test-pod-network", + "cniVersion": "0.3.1", + "plugins": [ + { + "type": "ptp", + "mtu": 1460, + "ipam": { + "type": "host-local", + "subnet": "10.0.0.0/24", + "ranges": [[{"subnet": "10.0.0.0/24"}], [{"subnet": "2001:4860:4860::/64"}]], + "routes": [{"dst": "0.0.0.0/0"}, {"dst": "::/0"}] + } + }, + ] +}` + ) + + for name, test := range map[string]struct { + noTemplate bool + emptyCIDR bool + networkReady bool + expectCNIConfig bool + }{ + "should not generate cni config if cidr is empty": { + emptyCIDR: true, + expectCNIConfig: false, + }, + "should not generate cni config if template file is not specified": { + noTemplate: true, + expectCNIConfig: false, + }, + "should not generate cni config if network is ready": { + networkReady: true, + expectCNIConfig: false, + }, + "should generate cni config if template is specified and cidr is provided": { + expectCNIConfig: true, + }, + } { + t.Run(name, func(t *testing.T) { + testDir, err := ioutil.TempDir(os.TempDir(), "test-runtime-config") + require.NoError(t, err) + defer os.RemoveAll(testDir) + templateName := filepath.Join(testDir, "template") + err = ioutil.WriteFile(templateName, []byte(testTemplate), 0666) + require.NoError(t, err) + confDir := filepath.Join(testDir, "net.d") + confName := filepath.Join(confDir, cniConfigFileName) + + c := newTestCRIService() + c.config.CniConfig = criconfig.CniConfig{ + NetworkPluginConfDir: confDir, + NetworkPluginConfTemplate: templateName, + } + req := &runtime.UpdateRuntimeConfigRequest{ + RuntimeConfig: &runtime.RuntimeConfig{ + NetworkConfig: &runtime.NetworkConfig{ + PodCidr: testCIDR, + }, + }, + } + if test.noTemplate { + c.config.CniConfig.NetworkPluginConfTemplate = "" + } + if test.emptyCIDR { + req.RuntimeConfig.NetworkConfig.PodCidr = "" + } + if !test.networkReady { + c.netPlugin.(*servertesting.FakeCNIPlugin).StatusErr = errors.New("random error") + c.netPlugin.(*servertesting.FakeCNIPlugin).LoadErr = errors.New("random error") + } + _, err = c.UpdateRuntimeConfig(context.Background(), req) + assert.NoError(t, err) + if !test.expectCNIConfig { + _, err := os.Stat(confName) + assert.Error(t, err) + } else { + got, err := ioutil.ReadFile(confName) + assert.NoError(t, err) + assert.Equal(t, expected, string(got)) + } + }) + } +} diff --git a/vendor/github.com/containerd/cri/pkg/server/version.go b/pkg/server/version.go similarity index 96% rename from vendor/github.com/containerd/cri/pkg/server/version.go rename to pkg/server/version.go index c1dea50c1..8c0e88611 100644 --- a/vendor/github.com/containerd/cri/pkg/server/version.go +++ b/pkg/server/version.go @@ -21,7 +21,7 @@ import ( "golang.org/x/net/context" runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" - "github.com/containerd/cri/pkg/constants" + "github.com/containerd/containerd/pkg/constants" ) const ( diff --git a/vendor/github.com/containerd/cri/pkg/seutil/seutil.go b/pkg/seutil/seutil.go similarity index 100% rename from vendor/github.com/containerd/cri/pkg/seutil/seutil.go rename to pkg/seutil/seutil.go diff --git a/vendor/github.com/containerd/cri/pkg/store/container/container.go b/pkg/store/container/container.go similarity index 96% rename from vendor/github.com/containerd/cri/pkg/store/container/container.go rename to pkg/store/container/container.go index 53c0745a5..1b75113a5 100644 --- a/vendor/github.com/containerd/cri/pkg/store/container/container.go +++ b/pkg/store/container/container.go @@ -20,12 +20,12 @@ import ( "sync" "github.com/containerd/containerd" - "github.com/containerd/cri/pkg/store/label" + "github.com/containerd/containerd/pkg/store/label" "github.com/docker/docker/pkg/truncindex" runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" - cio "github.com/containerd/cri/pkg/server/io" - "github.com/containerd/cri/pkg/store" + cio "github.com/containerd/containerd/pkg/server/io" + "github.com/containerd/containerd/pkg/store" ) // Container contains all resources associated with the container. All methods to diff --git a/pkg/store/container/container_test.go b/pkg/store/container/container_test.go new file mode 100644 index 000000000..9ab264b37 --- /dev/null +++ b/pkg/store/container/container_test.go @@ -0,0 +1,247 @@ +/* + Copyright The containerd 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 + + http://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. +*/ + +package container + +import ( + "strings" + "testing" + "time" + + "github.com/containerd/containerd/pkg/store/label" + "github.com/opencontainers/selinux/go-selinux" + assertlib "github.com/stretchr/testify/assert" + runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" + + cio "github.com/containerd/containerd/pkg/server/io" + "github.com/containerd/containerd/pkg/store" +) + +func TestContainerStore(t *testing.T) { + metadatas := map[string]Metadata{ + "1": { + ID: "1", + Name: "Container-1", + SandboxID: "Sandbox-1", + Config: &runtime.ContainerConfig{ + Metadata: &runtime.ContainerMetadata{ + Name: "TestPod-1", + Attempt: 1, + }, + }, + ImageRef: "TestImage-1", + StopSignal: "SIGTERM", + LogPath: "/test/log/path/1", + ProcessLabel: "junk:junk:junk:c1,c2", + }, + "2abcd": { + ID: "2abcd", + Name: "Container-2abcd", + SandboxID: "Sandbox-2abcd", + Config: &runtime.ContainerConfig{ + Metadata: &runtime.ContainerMetadata{ + Name: "TestPod-2abcd", + Attempt: 2, + }, + }, + StopSignal: "SIGTERM", + ImageRef: "TestImage-2", + LogPath: "/test/log/path/2", + ProcessLabel: "junk:junk:junk:c1,c2", + }, + "4a333": { + ID: "4a333", + Name: "Container-4a333", + SandboxID: "Sandbox-4a333", + Config: &runtime.ContainerConfig{ + Metadata: &runtime.ContainerMetadata{ + Name: "TestPod-4a333", + Attempt: 3, + }, + }, + StopSignal: "SIGTERM", + ImageRef: "TestImage-3", + LogPath: "/test/log/path/3", + ProcessLabel: "junk:junk:junk:c1,c3", + }, + "4abcd": { + ID: "4abcd", + Name: "Container-4abcd", + SandboxID: "Sandbox-4abcd", + Config: &runtime.ContainerConfig{ + Metadata: &runtime.ContainerMetadata{ + Name: "TestPod-4abcd", + Attempt: 1, + }, + }, + StopSignal: "SIGTERM", + ImageRef: "TestImage-4abcd", + ProcessLabel: "junk:junk:junk:c1,c4", + }, + } + statuses := map[string]Status{ + "1": { + Pid: 1, + CreatedAt: time.Now().UnixNano(), + StartedAt: time.Now().UnixNano(), + FinishedAt: time.Now().UnixNano(), + ExitCode: 1, + Reason: "TestReason-1", + Message: "TestMessage-1", + }, + "2abcd": { + Pid: 2, + CreatedAt: time.Now().UnixNano(), + StartedAt: time.Now().UnixNano(), + FinishedAt: time.Now().UnixNano(), + ExitCode: 2, + Reason: "TestReason-2abcd", + Message: "TestMessage-2abcd", + }, + "4a333": { + Pid: 3, + CreatedAt: time.Now().UnixNano(), + StartedAt: time.Now().UnixNano(), + FinishedAt: time.Now().UnixNano(), + ExitCode: 3, + Reason: "TestReason-4a333", + Message: "TestMessage-4a333", + Starting: true, + }, + "4abcd": { + Pid: 4, + CreatedAt: time.Now().UnixNano(), + StartedAt: time.Now().UnixNano(), + FinishedAt: time.Now().UnixNano(), + ExitCode: 4, + Reason: "TestReason-4abcd", + Message: "TestMessage-4abcd", + Removing: true, + }, + } + assert := assertlib.New(t) + containers := map[string]Container{} + for id := range metadatas { + container, err := NewContainer( + metadatas[id], + WithFakeStatus(statuses[id]), + ) + assert.NoError(err) + containers[id] = container + } + + s := NewStore(label.NewStore()) + reserved := map[string]bool{} + s.labels.Reserver = func(label string) { + reserved[strings.SplitN(label, ":", 4)[3]] = true + } + s.labels.Releaser = func(label string) { + reserved[strings.SplitN(label, ":", 4)[3]] = false + } + + t.Logf("should be able to add container") + for _, c := range containers { + assert.NoError(s.Add(c)) + } + + t.Logf("should be able to get container") + genTruncIndex := func(normalName string) string { return normalName[:(len(normalName)+1)/2] } + for id, c := range containers { + got, err := s.Get(genTruncIndex(id)) + assert.NoError(err) + assert.Equal(c, got) + } + + t.Logf("should be able to list containers") + cs := s.List() + assert.Len(cs, len(containers)) + + if selinux.GetEnabled() { + t.Logf("should have reserved labels (requires -tag selinux)") + assert.Equal(map[string]bool{ + "c1,c2": true, + "c1,c3": true, + "c1,c4": true, + }, reserved) + } + + cntrNum := len(containers) + for testID, v := range containers { + truncID := genTruncIndex(testID) + + t.Logf("add should return already exists error for duplicated container") + assert.Equal(store.ErrAlreadyExist, s.Add(v)) + + t.Logf("should be able to delete container") + s.Delete(truncID) + cntrNum-- + cs = s.List() + assert.Len(cs, cntrNum) + + t.Logf("get should return not exist error after deletion") + c, err := s.Get(truncID) + assert.Equal(Container{}, c) + assert.Equal(store.ErrNotExist, err) + } + + if selinux.GetEnabled() { + t.Logf("should have released all labels (requires -tag selinux)") + assert.Equal(map[string]bool{ + "c1,c2": false, + "c1,c3": false, + "c1,c4": false, + }, reserved) + } +} + +func TestWithContainerIO(t *testing.T) { + meta := Metadata{ + ID: "1", + Name: "Container-1", + SandboxID: "Sandbox-1", + Config: &runtime.ContainerConfig{ + Metadata: &runtime.ContainerMetadata{ + Name: "TestPod-1", + Attempt: 1, + }, + }, + ImageRef: "TestImage-1", + StopSignal: "SIGTERM", + LogPath: "/test/log/path", + } + status := Status{ + Pid: 1, + CreatedAt: time.Now().UnixNano(), + StartedAt: time.Now().UnixNano(), + FinishedAt: time.Now().UnixNano(), + ExitCode: 1, + Reason: "TestReason-1", + Message: "TestMessage-1", + } + assert := assertlib.New(t) + + c, err := NewContainer(meta, WithFakeStatus(status)) + assert.NoError(err) + assert.Nil(c.IO) + + c, err = NewContainer( + meta, + WithFakeStatus(status), + WithContainerIO(&cio.ContainerIO{}), + ) + assert.NoError(err) + assert.NotNil(c.IO) +} diff --git a/vendor/github.com/containerd/cri/pkg/store/container/fake_status.go b/pkg/store/container/fake_status.go similarity index 100% rename from vendor/github.com/containerd/cri/pkg/store/container/fake_status.go rename to pkg/store/container/fake_status.go diff --git a/vendor/github.com/containerd/cri/pkg/store/container/metadata.go b/pkg/store/container/metadata.go similarity index 100% rename from vendor/github.com/containerd/cri/pkg/store/container/metadata.go rename to pkg/store/container/metadata.go diff --git a/pkg/store/container/metadata_test.go b/pkg/store/container/metadata_test.go new file mode 100644 index 000000000..297bc094e --- /dev/null +++ b/pkg/store/container/metadata_test.go @@ -0,0 +1,81 @@ +/* + Copyright The containerd 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 + + http://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. +*/ + +package container + +import ( + "encoding/json" + "testing" + + assertlib "github.com/stretchr/testify/assert" + runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" +) + +func TestMetadataMarshalUnmarshal(t *testing.T) { + meta := &Metadata{ + ID: "test-id", + Name: "test-name", + SandboxID: "test-sandbox-id", + Config: &runtime.ContainerConfig{ + Metadata: &runtime.ContainerMetadata{ + Name: "test-name", + Attempt: 1, + }, + }, + ImageRef: "test-image-ref", + LogPath: "/test/log/path", + } + + assert := assertlib.New(t) + newMeta := &Metadata{} + newVerMeta := &versionedMetadata{} + + t.Logf("should be able to do json.marshal") + data, err := json.Marshal(meta) + assert.NoError(err) + data1, err := json.Marshal(&versionedMetadata{ + Version: metadataVersion, + Metadata: metadataInternal(*meta), + }) + assert.NoError(err) + assert.Equal(data, data1) + + t.Logf("should be able to do MarshalJSON") + data, err = meta.MarshalJSON() + assert.NoError(err) + assert.NoError(newMeta.UnmarshalJSON(data)) + assert.Equal(meta, newMeta) + + t.Logf("should be able to do MarshalJSON and json.Unmarshal") + data, err = meta.MarshalJSON() + assert.NoError(err) + assert.NoError(json.Unmarshal(data, newVerMeta)) + assert.Equal(meta, (*Metadata)(&newVerMeta.Metadata)) + + t.Logf("should be able to do json.Marshal and UnmarshalJSON") + data, err = json.Marshal(meta) + assert.NoError(err) + assert.NoError(newMeta.UnmarshalJSON(data)) + assert.Equal(meta, newMeta) + + t.Logf("should json.Unmarshal fail for unsupported version") + unsupported, err := json.Marshal(&versionedMetadata{ + Version: "random-test-version", + Metadata: metadataInternal(*meta), + }) + assert.NoError(err) + assert.Error(json.Unmarshal(unsupported, &newMeta)) +} diff --git a/vendor/github.com/containerd/cri/pkg/store/container/status.go b/pkg/store/container/status.go similarity index 100% rename from vendor/github.com/containerd/cri/pkg/store/container/status.go rename to pkg/store/container/status.go diff --git a/pkg/store/container/status_test.go b/pkg/store/container/status_test.go new file mode 100644 index 000000000..702cc262d --- /dev/null +++ b/pkg/store/container/status_test.go @@ -0,0 +1,195 @@ +/* + Copyright The containerd 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 + + http://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. +*/ + +package container + +import ( + "encoding/json" + "errors" + "io/ioutil" + "os" + "path/filepath" + "testing" + "time" + + assertlib "github.com/stretchr/testify/assert" + requirelib "github.com/stretchr/testify/require" + runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" +) + +func TestContainerState(t *testing.T) { + for c, test := range map[string]struct { + status Status + state runtime.ContainerState + }{ + "unknown state": { + status: Status{ + Unknown: true, + }, + state: runtime.ContainerState_CONTAINER_UNKNOWN, + }, + "unknown state because there is no timestamp set": { + status: Status{}, + state: runtime.ContainerState_CONTAINER_UNKNOWN, + }, + "created state": { + status: Status{ + CreatedAt: time.Now().UnixNano(), + }, + state: runtime.ContainerState_CONTAINER_CREATED, + }, + "running state": { + status: Status{ + CreatedAt: time.Now().UnixNano(), + StartedAt: time.Now().UnixNano(), + }, + state: runtime.ContainerState_CONTAINER_RUNNING, + }, + "exited state": { + status: Status{ + CreatedAt: time.Now().UnixNano(), + FinishedAt: time.Now().UnixNano(), + }, + state: runtime.ContainerState_CONTAINER_EXITED, + }, + } { + t.Logf("TestCase %q", c) + assertlib.Equal(t, test.state, test.status.State()) + } +} + +func TestStatusEncodeDecode(t *testing.T) { + s := &Status{ + Pid: 1234, + CreatedAt: time.Now().UnixNano(), + StartedAt: time.Now().UnixNano(), + FinishedAt: time.Now().UnixNano(), + ExitCode: 1, + Reason: "test-reason", + Message: "test-message", + Removing: true, + Starting: true, + Unknown: true, + } + assert := assertlib.New(t) + data, err := s.encode() + assert.NoError(err) + newS := &Status{} + assert.NoError(newS.decode(data)) + s.Removing = false // Removing should not be encoded. + s.Starting = false // Starting should not be encoded. + s.Unknown = false // Unknown should not be encoded. + assert.Equal(s, newS) + + unsupported, err := json.Marshal(&versionedStatus{ + Version: "random-test-version", + Status: *s, + }) + assert.NoError(err) + assert.Error(newS.decode(unsupported)) +} + +func TestStatus(t *testing.T) { + testID := "test-id" + testStatus := Status{ + CreatedAt: time.Now().UnixNano(), + } + updateStatus := Status{ + CreatedAt: time.Now().UnixNano(), + StartedAt: time.Now().UnixNano(), + } + updateErr := errors.New("update error") + assert := assertlib.New(t) + require := requirelib.New(t) + + tempDir, err := ioutil.TempDir(os.TempDir(), "status-test") + require.NoError(err) + defer os.RemoveAll(tempDir) + statusFile := filepath.Join(tempDir, "status") + + t.Logf("simple store and get") + s, err := StoreStatus(tempDir, testID, testStatus) + assert.NoError(err) + old := s.Get() + assert.Equal(testStatus, old) + _, err = os.Stat(statusFile) + assert.NoError(err) + loaded, err := LoadStatus(tempDir, testID) + require.NoError(err) + assert.Equal(testStatus, loaded) + + t.Logf("failed update should not take effect") + err = s.Update(func(o Status) (Status, error) { + o = updateStatus + return o, updateErr + }) + assert.Equal(updateErr, err) + assert.Equal(testStatus, s.Get()) + loaded, err = LoadStatus(tempDir, testID) + require.NoError(err) + assert.Equal(testStatus, loaded) + + t.Logf("successful update should take effect but not checkpoint") + err = s.Update(func(o Status) (Status, error) { + o = updateStatus + return o, nil + }) + assert.NoError(err) + assert.Equal(updateStatus, s.Get()) + loaded, err = LoadStatus(tempDir, testID) + require.NoError(err) + assert.Equal(testStatus, loaded) + // Recover status. + assert.NoError(s.Update(func(o Status) (Status, error) { + o = testStatus + return o, nil + })) + + t.Logf("failed update sync should not take effect") + err = s.UpdateSync(func(o Status) (Status, error) { + o = updateStatus + return o, updateErr + }) + assert.Equal(updateErr, err) + assert.Equal(testStatus, s.Get()) + loaded, err = LoadStatus(tempDir, testID) + require.NoError(err) + assert.Equal(testStatus, loaded) + + t.Logf("successful update sync should take effect and checkpoint") + err = s.UpdateSync(func(o Status) (Status, error) { + o = updateStatus + return o, nil + }) + assert.NoError(err) + assert.Equal(updateStatus, s.Get()) + loaded, err = LoadStatus(tempDir, testID) + require.NoError(err) + assert.Equal(updateStatus, loaded) + + t.Logf("successful update should not affect existing snapshot") + assert.Equal(testStatus, old) + + t.Logf("delete status") + assert.NoError(s.Delete()) + _, err = LoadStatus(tempDir, testID) + assert.Error(err) + _, err = os.Stat(statusFile) + assert.True(os.IsNotExist(err)) + + t.Logf("delete status should be idempotent") + assert.NoError(s.Delete()) +} diff --git a/vendor/github.com/containerd/cri/pkg/store/errors.go b/pkg/store/errors.go similarity index 100% rename from vendor/github.com/containerd/cri/pkg/store/errors.go rename to pkg/store/errors.go diff --git a/pkg/store/errors_test.go b/pkg/store/errors_test.go new file mode 100644 index 000000000..4171e0b37 --- /dev/null +++ b/pkg/store/errors_test.go @@ -0,0 +1,48 @@ +/* + Copyright The containerd 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 + + http://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. +*/ + +package store + +import ( + "testing" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/containerd/containerd/errdefs" +) + +func TestStoreErrAlreadyExistGRPCStatus(t *testing.T) { + err := errdefs.ToGRPC(ErrAlreadyExist) + s, ok := status.FromError(err) + if !ok { + t.Fatalf("failed to convert err: %v to status: %d", err, codes.AlreadyExists) + } + if s.Code() != codes.AlreadyExists { + t.Fatalf("expected code: %d got: %d", codes.AlreadyExists, s.Code()) + } +} + +func TestStoreErrNotExistGRPCStatus(t *testing.T) { + err := errdefs.ToGRPC(ErrNotExist) + s, ok := status.FromError(err) + if !ok { + t.Fatalf("failed to convert err: %v to status: %d", err, codes.NotFound) + } + if s.Code() != codes.NotFound { + t.Fatalf("expected code: %d got: %d", codes.NotFound, s.Code()) + } +} diff --git a/vendor/github.com/containerd/cri/pkg/store/image/fake_image.go b/pkg/store/image/fake_image.go similarity index 100% rename from vendor/github.com/containerd/cri/pkg/store/image/fake_image.go rename to pkg/store/image/fake_image.go diff --git a/vendor/github.com/containerd/cri/pkg/store/image/image.go b/pkg/store/image/image.go similarity index 98% rename from vendor/github.com/containerd/cri/pkg/store/image/image.go rename to pkg/store/image/image.go index 208d490db..cb70701c3 100644 --- a/vendor/github.com/containerd/cri/pkg/store/image/image.go +++ b/pkg/store/image/image.go @@ -30,8 +30,8 @@ import ( imagespec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/pkg/errors" - storeutil "github.com/containerd/cri/pkg/store" - "github.com/containerd/cri/pkg/util" + storeutil "github.com/containerd/containerd/pkg/store" + "github.com/containerd/containerd/pkg/util" ) // Image contains all resources associated with the image. All fields diff --git a/pkg/store/image/image_test.go b/pkg/store/image/image_test.go new file mode 100644 index 000000000..5f5fcc2d2 --- /dev/null +++ b/pkg/store/image/image_test.go @@ -0,0 +1,248 @@ +/* + Copyright The containerd 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 + + http://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. +*/ + +package image + +import ( + "sort" + "strings" + "testing" + + "github.com/opencontainers/go-digest/digestset" + assertlib "github.com/stretchr/testify/assert" + + storeutil "github.com/containerd/containerd/pkg/store" +) + +func TestInternalStore(t *testing.T) { + images := []Image{ + { + ID: "sha256:1123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + ChainID: "test-chain-id-1", + References: []string{"ref-1"}, + Size: 10, + }, + { + ID: "sha256:2123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + ChainID: "test-chain-id-2abcd", + References: []string{"ref-2abcd"}, + Size: 20, + }, + { + ID: "sha256:3123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + References: []string{"ref-4a333"}, + ChainID: "test-chain-id-4a333", + Size: 30, + }, + { + ID: "sha256:4123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + References: []string{"ref-4abcd"}, + ChainID: "test-chain-id-4abcd", + Size: 40, + }, + } + assert := assertlib.New(t) + genTruncIndex := func(normalName string) string { return normalName[:(len(normalName)+1)/2] } + + s := &store{ + images: make(map[string]Image), + digestSet: digestset.NewSet(), + } + + t.Logf("should be able to add image") + for _, img := range images { + err := s.add(img) + assert.NoError(err) + } + + t.Logf("should be able to get image") + for _, v := range images { + truncID := genTruncIndex(v.ID) + got, err := s.get(truncID) + assert.NoError(err, "truncID:%s, fullID:%s", truncID, v.ID) + assert.Equal(v, got) + } + + t.Logf("should be able to get image by truncated imageId without algorithm") + for _, v := range images { + truncID := genTruncIndex(v.ID[strings.Index(v.ID, ":")+1:]) + got, err := s.get(truncID) + assert.NoError(err, "truncID:%s, fullID:%s", truncID, v.ID) + assert.Equal(v, got) + } + + t.Logf("should not be able to get image by ambiguous prefix") + ambiguousPrefixs := []string{"sha256", "sha256:"} + for _, v := range ambiguousPrefixs { + _, err := s.get(v) + assert.NotEqual(nil, err) + } + + t.Logf("should be able to list images") + imgs := s.list() + assert.Len(imgs, len(images)) + + imageNum := len(images) + for _, v := range images { + truncID := genTruncIndex(v.ID) + oldRef := v.References[0] + newRef := oldRef + "new" + + t.Logf("should be able to add new references") + newImg := v + newImg.References = []string{newRef} + err := s.add(newImg) + assert.NoError(err) + got, err := s.get(truncID) + assert.NoError(err) + assert.Len(got.References, 2) + assert.Contains(got.References, oldRef, newRef) + + t.Logf("should not be able to add duplicated references") + err = s.add(newImg) + assert.NoError(err) + got, err = s.get(truncID) + assert.NoError(err) + assert.Len(got.References, 2) + assert.Contains(got.References, oldRef, newRef) + + t.Logf("should be able to delete image references") + s.delete(truncID, oldRef) + got, err = s.get(truncID) + assert.NoError(err) + assert.Equal([]string{newRef}, got.References) + + t.Logf("should be able to delete image") + s.delete(truncID, newRef) + got, err = s.get(truncID) + assert.Equal(storeutil.ErrNotExist, err) + assert.Equal(Image{}, got) + + imageNum-- + imgs = s.list() + assert.Len(imgs, imageNum) + } +} + +func TestImageStore(t *testing.T) { + id := "sha256:1123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + newID := "sha256:9923456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + image := Image{ + ID: id, + ChainID: "test-chain-id-1", + References: []string{"ref-1"}, + Size: 10, + } + assert := assertlib.New(t) + + equal := func(i1, i2 Image) { + sort.Strings(i1.References) + sort.Strings(i2.References) + assert.Equal(i1, i2) + } + for desc, test := range map[string]struct { + ref string + image *Image + expected []Image + }{ + "nothing should happen if a non-exist ref disappear": { + ref: "ref-2", + image: nil, + expected: []Image{image}, + }, + "new ref for an existing image": { + ref: "ref-2", + image: &Image{ + ID: id, + ChainID: "test-chain-id-1", + References: []string{"ref-2"}, + Size: 10, + }, + expected: []Image{ + { + ID: id, + ChainID: "test-chain-id-1", + References: []string{"ref-1", "ref-2"}, + Size: 10, + }, + }, + }, + "new ref for a new image": { + ref: "ref-2", + image: &Image{ + ID: newID, + ChainID: "test-chain-id-2", + References: []string{"ref-2"}, + Size: 20, + }, + expected: []Image{ + image, + { + ID: newID, + ChainID: "test-chain-id-2", + References: []string{"ref-2"}, + Size: 20, + }, + }, + }, + "existing ref point to a new image": { + ref: "ref-1", + image: &Image{ + ID: newID, + ChainID: "test-chain-id-2", + References: []string{"ref-1"}, + Size: 20, + }, + expected: []Image{ + { + ID: newID, + ChainID: "test-chain-id-2", + References: []string{"ref-1"}, + Size: 20, + }, + }, + }, + "existing ref disappear": { + ref: "ref-1", + image: nil, + expected: []Image{}, + }, + } { + t.Logf("TestCase %q", desc) + s, err := NewFakeStore([]Image{image}) + assert.NoError(err) + assert.NoError(s.update(test.ref, test.image)) + + assert.Len(s.List(), len(test.expected)) + for _, expect := range test.expected { + got, err := s.Get(expect.ID) + assert.NoError(err) + equal(got, expect) + for _, ref := range expect.References { + id, err := s.Resolve(ref) + assert.NoError(err) + assert.Equal(expect.ID, id) + } + } + + if test.image == nil { + // Shouldn't be able to index by removed ref. + id, err := s.Resolve(test.ref) + assert.Equal(storeutil.ErrNotExist, err) + assert.Empty(id) + } + } +} diff --git a/vendor/github.com/containerd/cri/pkg/store/label/label.go b/pkg/store/label/label.go similarity index 100% rename from vendor/github.com/containerd/cri/pkg/store/label/label.go rename to pkg/store/label/label.go diff --git a/pkg/store/label/label_test.go b/pkg/store/label/label_test.go new file mode 100644 index 000000000..cc2c214bf --- /dev/null +++ b/pkg/store/label/label_test.go @@ -0,0 +1,116 @@ +/* + Copyright The containerd 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 + + http://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. +*/ + +package label + +import ( + "testing" + + "github.com/opencontainers/selinux/go-selinux" + "github.com/stretchr/testify/assert" +) + +func TestAddThenRemove(t *testing.T) { + if !selinux.GetEnabled() { + t.Skip("selinux is not enabled") + } + + assert := assert.New(t) + store := NewStore() + releaseCount := 0 + reserveCount := 0 + store.Releaser = func(label string) { + assert.Contains(label, ":c1,c2") + releaseCount++ + assert.Equal(1, releaseCount) + } + store.Reserver = func(label string) { + assert.Contains(label, ":c1,c2") + reserveCount++ + assert.Equal(1, reserveCount) + } + + t.Log("should count to two level") + assert.NoError(store.Reserve("junk:junk:junk:c1,c2")) + assert.NoError(store.Reserve("junk2:junk2:junk2:c1,c2")) + + t.Log("should have one item") + assert.Equal(1, len(store.levels)) + + t.Log("c1,c2 count should be 2") + assert.Equal(2, store.levels["c1,c2"]) + + store.Release("junk:junk:junk:c1,c2") + store.Release("junk2:junk2:junk2:c1,c2") + + t.Log("should have 0 items") + assert.Equal(0, len(store.levels)) + + t.Log("should have reserved") + assert.Equal(1, reserveCount) + + t.Log("should have released") + assert.Equal(1, releaseCount) +} + +func TestJunkData(t *testing.T) { + if !selinux.GetEnabled() { + t.Skip("selinux is not enabled") + } + + assert := assert.New(t) + store := NewStore() + releaseCount := 0 + store.Releaser = func(label string) { + releaseCount++ + } + reserveCount := 0 + store.Reserver = func(label string) { + reserveCount++ + } + + t.Log("should ignore empty label") + assert.NoError(store.Reserve("")) + assert.Equal(0, len(store.levels)) + store.Release("") + assert.Equal(0, len(store.levels)) + assert.Equal(0, releaseCount) + assert.Equal(0, reserveCount) + + t.Log("should fail on bad label") + assert.Error(store.Reserve("junkjunkjunkc1c2")) + assert.Equal(0, len(store.levels)) + store.Release("junkjunkjunkc1c2") + assert.Equal(0, len(store.levels)) + assert.Equal(0, releaseCount) + assert.Equal(0, reserveCount) + + t.Log("should not release unknown label") + store.Release("junk2:junk2:junk2:c1,c2") + assert.Equal(0, len(store.levels)) + assert.Equal(0, releaseCount) + assert.Equal(0, reserveCount) + + t.Log("should release once even if too many deletes") + assert.NoError(store.Reserve("junk2:junk2:junk2:c1,c2")) + assert.Equal(1, len(store.levels)) + assert.Equal(1, store.levels["c1,c2"]) + store.Release("junk2:junk2:junk2:c1,c2") + store.Release("junk2:junk2:junk2:c1,c2") + assert.Equal(0, len(store.levels)) + assert.Equal(1, releaseCount) + assert.Equal(1, reserveCount) +} diff --git a/vendor/github.com/containerd/cri/pkg/store/sandbox/metadata.go b/pkg/store/sandbox/metadata.go similarity index 100% rename from vendor/github.com/containerd/cri/pkg/store/sandbox/metadata.go rename to pkg/store/sandbox/metadata.go diff --git a/pkg/store/sandbox/metadata_test.go b/pkg/store/sandbox/metadata_test.go new file mode 100644 index 000000000..d0a51d90e --- /dev/null +++ b/pkg/store/sandbox/metadata_test.go @@ -0,0 +1,79 @@ +/* + Copyright The containerd 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 + + http://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. +*/ + +package sandbox + +import ( + "encoding/json" + "testing" + + assertlib "github.com/stretchr/testify/assert" + runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" +) + +func TestMetadataMarshalUnmarshal(t *testing.T) { + meta := &Metadata{ + ID: "test-id", + Name: "test-name", + Config: &runtime.PodSandboxConfig{ + Metadata: &runtime.PodSandboxMetadata{ + Name: "test-name", + Uid: "test-uid", + Namespace: "test-namespace", + Attempt: 1, + }, + }, + } + assert := assertlib.New(t) + newMeta := &Metadata{} + newVerMeta := &versionedMetadata{} + + t.Logf("should be able to do json.marshal") + data, err := json.Marshal(meta) + assert.NoError(err) + data1, err := json.Marshal(&versionedMetadata{ + Version: metadataVersion, + Metadata: metadataInternal(*meta), + }) + assert.NoError(err) + assert.Equal(data, data1) + + t.Logf("should be able to do MarshalJSON") + data, err = meta.MarshalJSON() + assert.NoError(err) + assert.NoError(newMeta.UnmarshalJSON(data)) + assert.Equal(meta, newMeta) + + t.Logf("should be able to do MarshalJSON and json.Unmarshal") + data, err = meta.MarshalJSON() + assert.NoError(err) + assert.NoError(json.Unmarshal(data, newVerMeta)) + assert.Equal(meta, (*Metadata)(&newVerMeta.Metadata)) + + t.Logf("should be able to do json.Marshal and UnmarshalJSON") + data, err = json.Marshal(meta) + assert.NoError(err) + assert.NoError(newMeta.UnmarshalJSON(data)) + assert.Equal(meta, newMeta) + + t.Logf("should json.Unmarshal fail for unsupported version") + unsupported, err := json.Marshal(&versionedMetadata{ + Version: "random-test-version", + Metadata: metadataInternal(*meta), + }) + assert.NoError(err) + assert.Error(json.Unmarshal(unsupported, &newMeta)) +} diff --git a/vendor/github.com/containerd/cri/pkg/store/sandbox/sandbox.go b/pkg/store/sandbox/sandbox.go similarity index 96% rename from vendor/github.com/containerd/cri/pkg/store/sandbox/sandbox.go rename to pkg/store/sandbox/sandbox.go index 223e88369..3dc2029dd 100644 --- a/vendor/github.com/containerd/cri/pkg/store/sandbox/sandbox.go +++ b/pkg/store/sandbox/sandbox.go @@ -20,11 +20,11 @@ import ( "sync" "github.com/containerd/containerd" - "github.com/containerd/cri/pkg/store/label" + "github.com/containerd/containerd/pkg/store/label" "github.com/docker/docker/pkg/truncindex" - "github.com/containerd/cri/pkg/netns" - "github.com/containerd/cri/pkg/store" + "github.com/containerd/containerd/pkg/netns" + "github.com/containerd/containerd/pkg/store" ) // Sandbox contains all resources associated with the sandbox. All methods to diff --git a/pkg/store/sandbox/sandbox_test.go b/pkg/store/sandbox/sandbox_test.go new file mode 100644 index 000000000..eff3bb612 --- /dev/null +++ b/pkg/store/sandbox/sandbox_test.go @@ -0,0 +1,156 @@ +/* + Copyright The containerd 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 + + http://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. +*/ + +package sandbox + +import ( + "testing" + + "github.com/containerd/containerd/pkg/store/label" + assertlib "github.com/stretchr/testify/assert" + runtime "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" + + "github.com/containerd/containerd/pkg/store" +) + +func TestSandboxStore(t *testing.T) { + sandboxes := map[string]Sandbox{ + "1": NewSandbox( + Metadata{ + ID: "1", + Name: "Sandbox-1", + Config: &runtime.PodSandboxConfig{ + Metadata: &runtime.PodSandboxMetadata{ + Name: "TestPod-1", + Uid: "TestUid-1", + Namespace: "TestNamespace-1", + Attempt: 1, + }, + }, + NetNSPath: "TestNetNS-1", + }, + Status{State: StateReady}, + ), + "2abcd": NewSandbox( + Metadata{ + ID: "2abcd", + Name: "Sandbox-2abcd", + Config: &runtime.PodSandboxConfig{ + Metadata: &runtime.PodSandboxMetadata{ + Name: "TestPod-2abcd", + Uid: "TestUid-2abcd", + Namespace: "TestNamespace-2abcd", + Attempt: 2, + }, + }, + NetNSPath: "TestNetNS-2", + }, + Status{State: StateNotReady}, + ), + "4a333": NewSandbox( + Metadata{ + ID: "4a333", + Name: "Sandbox-4a333", + Config: &runtime.PodSandboxConfig{ + Metadata: &runtime.PodSandboxMetadata{ + Name: "TestPod-4a333", + Uid: "TestUid-4a333", + Namespace: "TestNamespace-4a333", + Attempt: 3, + }, + }, + NetNSPath: "TestNetNS-3", + }, + Status{State: StateNotReady}, + ), + "4abcd": NewSandbox( + Metadata{ + ID: "4abcd", + Name: "Sandbox-4abcd", + Config: &runtime.PodSandboxConfig{ + Metadata: &runtime.PodSandboxMetadata{ + Name: "TestPod-4abcd", + Uid: "TestUid-4abcd", + Namespace: "TestNamespace-4abcd", + Attempt: 1, + }, + }, + NetNSPath: "TestNetNS-4abcd", + }, + Status{State: StateReady}, + ), + } + unknown := NewSandbox( + Metadata{ + ID: "3defg", + Name: "Sandbox-3defg", + Config: &runtime.PodSandboxConfig{ + Metadata: &runtime.PodSandboxMetadata{ + Name: "TestPod-3defg", + Uid: "TestUid-3defg", + Namespace: "TestNamespace-3defg", + Attempt: 1, + }, + }, + NetNSPath: "TestNetNS-3defg", + }, + Status{State: StateUnknown}, + ) + assert := assertlib.New(t) + s := NewStore(label.NewStore()) + + t.Logf("should be able to add sandbox") + for _, sb := range sandboxes { + assert.NoError(s.Add(sb)) + } + assert.NoError(s.Add(unknown)) + + t.Logf("should be able to get sandbox") + genTruncIndex := func(normalName string) string { return normalName[:(len(normalName)+1)/2] } + for id, sb := range sandboxes { + got, err := s.Get(genTruncIndex(id)) + assert.NoError(err) + assert.Equal(sb, got) + } + + t.Logf("should be able to get sandbox in unknown state with Get") + got, err := s.Get(unknown.ID) + assert.NoError(err) + assert.Equal(unknown, got) + + t.Logf("should be able to list sandboxes") + sbNum := len(sandboxes) + 1 + sbs := s.List() + assert.Len(sbs, sbNum) + + for testID, v := range sandboxes { + truncID := genTruncIndex(testID) + + t.Logf("add should return already exists error for duplicated sandbox") + assert.Equal(store.ErrAlreadyExist, s.Add(v)) + + t.Logf("should be able to delete sandbox") + s.Delete(truncID) + sbNum-- + sbs = s.List() + assert.Len(sbs, sbNum) + + t.Logf("get should return not exist error after deletion") + sb, err := s.Get(truncID) + assert.Equal(Sandbox{}, sb) + assert.Equal(store.ErrNotExist, err) + } +} diff --git a/vendor/github.com/containerd/cri/pkg/store/sandbox/status.go b/pkg/store/sandbox/status.go similarity index 100% rename from vendor/github.com/containerd/cri/pkg/store/sandbox/status.go rename to pkg/store/sandbox/status.go diff --git a/pkg/store/sandbox/status_test.go b/pkg/store/sandbox/status_test.go new file mode 100644 index 000000000..ad27db0c6 --- /dev/null +++ b/pkg/store/sandbox/status_test.go @@ -0,0 +1,69 @@ +/* + Copyright The containerd 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 + + http://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. +*/ + +package sandbox + +import ( + "errors" + "testing" + "time" + + assertlib "github.com/stretchr/testify/assert" +) + +func TestStatus(t *testing.T) { + testStatus := Status{ + Pid: 123, + CreatedAt: time.Now(), + State: StateUnknown, + } + updateStatus := Status{ + Pid: 456, + CreatedAt: time.Now(), + State: StateReady, + } + updateErr := errors.New("update error") + assert := assertlib.New(t) + + t.Logf("simple store and get") + s := StoreStatus(testStatus) + old := s.Get() + assert.Equal(testStatus, old) + + t.Logf("failed update should not take effect") + err := s.Update(func(o Status) (Status, error) { + o = updateStatus + return o, updateErr + }) + assert.Equal(updateErr, err) + assert.Equal(testStatus, s.Get()) + + t.Logf("successful update should take effect but not checkpoint") + err = s.Update(func(o Status) (Status, error) { + o = updateStatus + return o, nil + }) + assert.NoError(err) + assert.Equal(updateStatus, s.Get()) +} + +func TestStateStringConversion(t *testing.T) { + assert := assertlib.New(t) + assert.Equal("SANDBOX_READY", StateReady.String()) + assert.Equal("SANDBOX_NOTREADY", StateNotReady.String()) + assert.Equal("SANDBOX_UNKNOWN", StateUnknown.String()) + assert.Equal("invalid sandbox state value: 123", State(123).String()) +} diff --git a/vendor/github.com/containerd/cri/pkg/store/snapshot/snapshot.go b/pkg/store/snapshot/snapshot.go similarity index 97% rename from vendor/github.com/containerd/cri/pkg/store/snapshot/snapshot.go rename to pkg/store/snapshot/snapshot.go index ce05f0e04..206f5f7b2 100644 --- a/vendor/github.com/containerd/cri/pkg/store/snapshot/snapshot.go +++ b/pkg/store/snapshot/snapshot.go @@ -21,7 +21,7 @@ import ( snapshot "github.com/containerd/containerd/snapshots" - "github.com/containerd/cri/pkg/store" + "github.com/containerd/containerd/pkg/store" ) // Snapshot contains the information about the snapshot. diff --git a/pkg/store/snapshot/snapshot_test.go b/pkg/store/snapshot/snapshot_test.go new file mode 100644 index 000000000..eff909d94 --- /dev/null +++ b/pkg/store/snapshot/snapshot_test.go @@ -0,0 +1,84 @@ +/* + Copyright The containerd 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 + + http://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. +*/ + +package snapshot + +import ( + "testing" + "time" + + snapshot "github.com/containerd/containerd/snapshots" + assertlib "github.com/stretchr/testify/assert" + + "github.com/containerd/containerd/pkg/store" +) + +func TestSnapshotStore(t *testing.T) { + snapshots := map[string]Snapshot{ + "key1": { + Key: "key1", + Kind: snapshot.KindActive, + Size: 10, + Inodes: 100, + Timestamp: time.Now().UnixNano(), + }, + "key2": { + Key: "key2", + Kind: snapshot.KindCommitted, + Size: 20, + Inodes: 200, + Timestamp: time.Now().UnixNano(), + }, + "key3": { + Key: "key3", + Kind: snapshot.KindView, + Size: 0, + Inodes: 0, + Timestamp: time.Now().UnixNano(), + }, + } + assert := assertlib.New(t) + + s := NewStore() + + t.Logf("should be able to add snapshot") + for _, sn := range snapshots { + s.Add(sn) + } + + t.Logf("should be able to get snapshot") + for id, sn := range snapshots { + got, err := s.Get(id) + assert.NoError(err) + assert.Equal(sn, got) + } + + t.Logf("should be able to list snapshot") + sns := s.List() + assert.Len(sns, 3) + + testKey := "key2" + + t.Logf("should be able to delete snapshot") + s.Delete(testKey) + sns = s.List() + assert.Len(sns, 2) + + t.Logf("get should return empty struct and ErrNotExist after deletion") + sn, err := s.Get(testKey) + assert.Equal(Snapshot{}, sn) + assert.Equal(store.ErrNotExist, err) +} diff --git a/vendor/github.com/containerd/cri/pkg/store/util.go b/pkg/store/util.go similarity index 100% rename from vendor/github.com/containerd/cri/pkg/store/util.go rename to pkg/store/util.go diff --git a/vendor/github.com/containerd/cri/pkg/streaming/errors.go b/pkg/streaming/errors.go similarity index 100% rename from vendor/github.com/containerd/cri/pkg/streaming/errors.go rename to pkg/streaming/errors.go diff --git a/vendor/github.com/containerd/cri/pkg/streaming/portforward/constants.go b/pkg/streaming/portforward/constants.go similarity index 100% rename from vendor/github.com/containerd/cri/pkg/streaming/portforward/constants.go rename to pkg/streaming/portforward/constants.go diff --git a/vendor/github.com/containerd/cri/pkg/streaming/portforward/httpstream.go b/pkg/streaming/portforward/httpstream.go similarity index 100% rename from vendor/github.com/containerd/cri/pkg/streaming/portforward/httpstream.go rename to pkg/streaming/portforward/httpstream.go diff --git a/vendor/github.com/containerd/cri/pkg/streaming/portforward/portforward.go b/pkg/streaming/portforward/portforward.go similarity index 100% rename from vendor/github.com/containerd/cri/pkg/streaming/portforward/portforward.go rename to pkg/streaming/portforward/portforward.go diff --git a/vendor/github.com/containerd/cri/pkg/streaming/portforward/websocket.go b/pkg/streaming/portforward/websocket.go similarity index 100% rename from vendor/github.com/containerd/cri/pkg/streaming/portforward/websocket.go rename to pkg/streaming/portforward/websocket.go diff --git a/vendor/github.com/containerd/cri/pkg/streaming/remotecommand/attach.go b/pkg/streaming/remotecommand/attach.go similarity index 100% rename from vendor/github.com/containerd/cri/pkg/streaming/remotecommand/attach.go rename to pkg/streaming/remotecommand/attach.go diff --git a/vendor/github.com/containerd/cri/pkg/streaming/remotecommand/doc.go b/pkg/streaming/remotecommand/doc.go similarity index 100% rename from vendor/github.com/containerd/cri/pkg/streaming/remotecommand/doc.go rename to pkg/streaming/remotecommand/doc.go diff --git a/vendor/github.com/containerd/cri/pkg/streaming/remotecommand/exec.go b/pkg/streaming/remotecommand/exec.go similarity index 100% rename from vendor/github.com/containerd/cri/pkg/streaming/remotecommand/exec.go rename to pkg/streaming/remotecommand/exec.go diff --git a/vendor/github.com/containerd/cri/pkg/streaming/remotecommand/httpstream.go b/pkg/streaming/remotecommand/httpstream.go similarity index 100% rename from vendor/github.com/containerd/cri/pkg/streaming/remotecommand/httpstream.go rename to pkg/streaming/remotecommand/httpstream.go diff --git a/vendor/github.com/containerd/cri/pkg/streaming/remotecommand/websocket.go b/pkg/streaming/remotecommand/websocket.go similarity index 100% rename from vendor/github.com/containerd/cri/pkg/streaming/remotecommand/websocket.go rename to pkg/streaming/remotecommand/websocket.go diff --git a/vendor/github.com/containerd/cri/pkg/streaming/request_cache.go b/pkg/streaming/request_cache.go similarity index 100% rename from vendor/github.com/containerd/cri/pkg/streaming/request_cache.go rename to pkg/streaming/request_cache.go diff --git a/vendor/github.com/containerd/cri/pkg/streaming/server.go b/pkg/streaming/server.go similarity index 98% rename from vendor/github.com/containerd/cri/pkg/streaming/server.go rename to pkg/streaming/server.go index 589c9a8ca..ee904dbda 100644 --- a/vendor/github.com/containerd/cri/pkg/streaming/server.go +++ b/pkg/streaming/server.go @@ -52,8 +52,8 @@ import ( "k8s.io/client-go/tools/remotecommand" runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" - "github.com/containerd/cri/pkg/streaming/portforward" - remotecommandserver "github.com/containerd/cri/pkg/streaming/remotecommand" + "github.com/containerd/containerd/pkg/streaming/portforward" + remotecommandserver "github.com/containerd/containerd/pkg/streaming/remotecommand" ) // Server is the library interface to serve the stream requests. diff --git a/vendor/github.com/containerd/cri/pkg/util/deep_copy.go b/pkg/util/deep_copy.go similarity index 100% rename from vendor/github.com/containerd/cri/pkg/util/deep_copy.go rename to pkg/util/deep_copy.go diff --git a/pkg/util/deep_copy_test.go b/pkg/util/deep_copy_test.go new file mode 100644 index 000000000..4ca1ebb16 --- /dev/null +++ b/pkg/util/deep_copy_test.go @@ -0,0 +1,63 @@ +/* + Copyright The containerd 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 + + http://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. +*/ + +package util + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +type A struct { + String string + Int int + Strings []string + Ints map[string]int + As map[string]*A +} + +func TestCopy(t *testing.T) { + src := &A{ + String: "Hello World", + Int: 5, + Strings: []string{"A", "B"}, + Ints: map[string]int{"A": 1, "B": 2, "C": 4}, + As: map[string]*A{ + "One": {String: "2"}, + "Two": {String: "3"}, + }, + } + dst := &A{ + Strings: []string{"C"}, + Ints: map[string]int{"B": 3, "C": 4}, + As: map[string]*A{"One": {String: "1", Int: 5}}, + } + expected := &A{ + String: "Hello World", + Int: 5, + Strings: []string{"A", "B"}, + Ints: map[string]int{"A": 1, "B": 2, "C": 4}, + As: map[string]*A{ + "One": {String: "2"}, + "Two": {String: "3"}, + }, + } + assert.NotEqual(t, expected, dst) + err := DeepCopy(dst, src) + assert.NoError(t, err) + assert.Equal(t, expected, dst) +} diff --git a/vendor/github.com/containerd/cri/pkg/util/id.go b/pkg/util/id.go similarity index 100% rename from vendor/github.com/containerd/cri/pkg/util/id.go rename to pkg/util/id.go diff --git a/vendor/github.com/containerd/cri/pkg/util/image.go b/pkg/util/image.go similarity index 100% rename from vendor/github.com/containerd/cri/pkg/util/image.go rename to pkg/util/image.go diff --git a/pkg/util/image_test.go b/pkg/util/image_test.go new file mode 100644 index 000000000..f4d911b1c --- /dev/null +++ b/pkg/util/image_test.go @@ -0,0 +1,84 @@ +/* + Copyright The containerd 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 + + http://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. +*/ + +package util + +import ( + "testing" + + "github.com/containerd/containerd/reference" + "github.com/stretchr/testify/assert" +) + +func TestNormalizeImageRef(t *testing.T) { + for _, test := range []struct { + input string + expect string + }{ + { // has nothing + input: "busybox", + expect: "docker.io/library/busybox:latest", + }, + { // only has tag + input: "busybox:latest", + expect: "docker.io/library/busybox:latest", + }, + { // only has digest + input: "busybox@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582", + expect: "docker.io/library/busybox@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582", + }, + { // only has path + input: "library/busybox", + expect: "docker.io/library/busybox:latest", + }, + { // only has hostname + input: "docker.io/busybox", + expect: "docker.io/library/busybox:latest", + }, + { // has no tag + input: "docker.io/library/busybox", + expect: "docker.io/library/busybox:latest", + }, + { // has no path + input: "docker.io/busybox:latest", + expect: "docker.io/library/busybox:latest", + }, + { // has no hostname + input: "library/busybox:latest", + expect: "docker.io/library/busybox:latest", + }, + { // full reference + input: "docker.io/library/busybox:latest", + expect: "docker.io/library/busybox:latest", + }, + { // gcr reference + input: "gcr.io/library/busybox", + expect: "gcr.io/library/busybox:latest", + }, + { // both tag and digest + input: "gcr.io/library/busybox:latest@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582", + expect: "gcr.io/library/busybox@sha256:e6693c20186f837fc393390135d8a598a96a833917917789d63766cab6c59582", + }, + } { + t.Logf("TestCase %q", test.input) + normalized, err := NormalizeImageRef(test.input) + assert.NoError(t, err) + output := normalized.String() + assert.Equal(t, test.expect, output) + _, err = reference.Parse(output) + assert.NoError(t, err, "%q should be containerd supported reference", output) + } +} diff --git a/vendor/github.com/containerd/cri/pkg/util/strings.go b/pkg/util/strings.go similarity index 100% rename from vendor/github.com/containerd/cri/pkg/util/strings.go rename to pkg/util/strings.go diff --git a/pkg/util/strings_test.go b/pkg/util/strings_test.go new file mode 100644 index 000000000..1679b5ab7 --- /dev/null +++ b/pkg/util/strings_test.go @@ -0,0 +1,59 @@ +/* + Copyright The containerd 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 + + http://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. +*/ + +package util + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestInStringSlice(t *testing.T) { + ss := []string{"ABC", "def", "ghi"} + + assert.True(t, InStringSlice(ss, "ABC")) + assert.True(t, InStringSlice(ss, "abc")) + assert.True(t, InStringSlice(ss, "def")) + assert.True(t, InStringSlice(ss, "DEF")) + assert.False(t, InStringSlice(ss, "hij")) + assert.False(t, InStringSlice(ss, "HIJ")) + assert.False(t, InStringSlice(nil, "HIJ")) +} + +func TestSubtractStringSlice(t *testing.T) { + ss := []string{"ABC", "def", "ghi"} + + assert.Equal(t, []string{"def", "ghi"}, SubtractStringSlice(ss, "abc")) + assert.Equal(t, []string{"def", "ghi"}, SubtractStringSlice(ss, "ABC")) + assert.Equal(t, []string{"ABC", "ghi"}, SubtractStringSlice(ss, "def")) + assert.Equal(t, []string{"ABC", "ghi"}, SubtractStringSlice(ss, "DEF")) + assert.Equal(t, []string{"ABC", "def", "ghi"}, SubtractStringSlice(ss, "hij")) + assert.Equal(t, []string{"ABC", "def", "ghi"}, SubtractStringSlice(ss, "HIJ")) + assert.Empty(t, SubtractStringSlice(nil, "hij")) + assert.Empty(t, SubtractStringSlice([]string{}, "hij")) +} + +func TestMergeStringSlices(t *testing.T) { + s1 := []string{"abc", "def", "ghi"} + s2 := []string{"def", "jkl", "mno"} + expect := []string{"abc", "def", "ghi", "jkl", "mno"} + result := MergeStringSlices(s1, s2) + assert.Len(t, result, len(expect)) + for _, s := range expect { + assert.Contains(t, result, s) + } +} diff --git a/vendor.conf b/vendor.conf index 432bf67d4..d87a7e7b7 100644 --- a/vendor.conf +++ b/vendor.conf @@ -57,7 +57,6 @@ gotest.tools/v3 v3.0.2 github.com/cilium/ebpf 1c8d4c9ef7759622653a1d319284a44652333b28 # cri dependencies -github.com/containerd/cri 210a86ca5bf6c8ca5f2553272d72c774b21fdec2 # master github.com/davecgh/go-spew v1.1.1 github.com/docker/docker 4634ce647cf2ce2c6031129ccd109e557244986f github.com/docker/spdystream 449fdfce4d962303d702fec724ef0ad181c92528 @@ -68,6 +67,8 @@ github.com/json-iterator/go v1.1.10 github.com/modern-go/concurrent 1.0.3 github.com/modern-go/reflect2 v1.0.1 github.com/opencontainers/selinux v1.6.0 +github.com/pmezard/go-difflib v1.0.0 +github.com/stretchr/testify v1.4.0 github.com/tchap/go-patricia v2.2.6 github.com/willf/bitset v1.1.11 golang.org/x/crypto 75b288015ac94e66e3d6715fb68a9b41bf046ec2 @@ -79,6 +80,7 @@ k8s.io/api v0.19.0-rc.4 k8s.io/apimachinery v0.19.0-rc.4 k8s.io/apiserver v0.19.0-rc.4 k8s.io/client-go v0.19.0-rc.4 +k8s.io/component-base v0.19.2 k8s.io/cri-api v0.19.0-rc.4 k8s.io/klog/v2 v2.2.0 k8s.io/utils 2df71ebbae66f39338aed4cd0bb82d2212ee33cc diff --git a/vendor/github.com/containerd/cri/README.md b/vendor/github.com/containerd/cri/README.md deleted file mode 100644 index 9f80a26a1..000000000 --- a/vendor/github.com/containerd/cri/README.md +++ /dev/null @@ -1,189 +0,0 @@ -# cri -

- - -

- -*Note: The standalone `cri-containerd` binary is end-of-life. `cri-containerd` is -transitioning from a standalone binary that talks to containerd to a plugin within -containerd. This github branch is for the `cri` plugin. See -[standalone-cri-containerd branch](https://github.com/containerd/cri/tree/standalone-cri-containerd) -for information about the standalone version of `cri-containerd`.* - -*Note: You need to [drain your node](https://kubernetes.io/docs/tasks/administer-cluster/safely-drain-node/) before upgrading from standalone `cri-containerd` to containerd with `cri` plugin.* - -[![Build Status](https://api.travis-ci.org/containerd/cri.svg?style=flat-square)](https://travis-ci.org/containerd/cri) -[![Go Report Card](https://goreportcard.com/badge/github.com/containerd/cri)](https://goreportcard.com/report/github.com/containerd/cri) - -`cri` is a [containerd](https://containerd.io/) plugin implementation of Kubernetes [container runtime interface (CRI)](https://github.com/kubernetes/cri-api/blob/master/pkg/apis/runtime/v1alpha2/api.proto). - -With it, you could run Kubernetes using containerd as the container runtime. -![cri](./docs/cri.png) -## Current Status -`cri` is a native plugin of containerd 1.1 and above. It is built into containerd and enabled by default. - -`cri` is in GA: -* It is feature complete. -* It (the GA version) works with Kubernetes 1.10 and above. -* It has passed all [CRI validation tests](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-node/cri-validation.md). -* It has passed all [node e2e tests](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-node/e2e-node-tests.md). -* It has passed all [e2e tests](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-testing/e2e-tests.md). - -See [test dashboard](https://k8s-testgrid.appspot.com/sig-node-containerd) -## Support Metrics -| CRI-Containerd Version | Containerd Version | Kubernetes Version | CRI Version | -|:----------------------:|:------------------:|:------------------:|:-----------:| -| v1.0.0-alpha.x | | 1.7, 1.8 | v1alpha1 | -| v1.0.0-beta.x | | 1.9 | v1alpha1 | -| End-Of-Life | v1.1 (End-Of-Life) | 1.10+ | v1alpha2 | -| | v1.2 (Extended) | 1.10+ | v1alpha2 | -| | v1.3 | 1.12+ | v1alpha2 | -| | v1.4 | 1.19+ (rc) | v1alpha2 | - -**Note:** The support table above specifies the Kubernetes Version that was supported at time of release of the containerd - cri integration. - -The following is the current support table for containerd CRI integration taking into account that Kubernetes only supports n-3 minor release versions. - -| Containerd Version | Kubernetes Version | CRI Version | -|:------------------:|:------------------:|:-----------:| -| v1.2 | 1.15+ | v1alpha2 | -| v1.3 | 1.15+ | v1alpha2 | -| v1.4 | 1.19+ (rc) | v1alpha2 | - -## Production Quality Cluster on GCE -For a production quality cluster on GCE brought up with `kube-up.sh` refer [here](docs/kube-up.md). -## Installing with Ansible and Kubeadm -For a multi node cluster installer and bring up steps using ansible and kubeadm refer [here](contrib/ansible/README.md). -## Custom Installation -For non ansible users, you can download the `cri-containerd` release tarball and deploy -kubernetes cluster using kubeadm as described [here](docs/installation.md). -## Getting Started for Developers -### Binary Dependencies and Specifications -The current release of the `cri` plugin has the following dependencies: -* [containerd](https://github.com/containerd/containerd) -* [runc](https://github.com/opencontainers/runc) -* [CNI](https://github.com/containernetworking/cni) - -See [versions](./vendor.conf) of these dependencies `cri` is tested with. - -As containerd and runc move to their respective general availability releases, -we will do our best to rebase/retest `cri` with these releases on a -weekly/monthly basis. Similarly, given that `cri` uses the Open -Container Initiative (OCI) [image](https://github.com/opencontainers/image-spec) -and [runtime](https://github.com/opencontainers/runtime-spec) specifications, we -will also do our best to update `cri` to the latest releases of these -specifications as appropriate. -### Install Dependencies -1. Install development libraries: -* **libseccomp development library.** Required by `cri` and runc seccomp support. `libseccomp-dev` (Ubuntu, Debian) / `libseccomp-devel` -(Fedora, CentOS, RHEL). On releases of Ubuntu <=Trusty and Debian <=jessie a -backport version of `libseccomp-dev` is required. See [travis.yml](.travis.yml) for an example on trusty. -* **btrfs development library.** Required by containerd btrfs support. `btrfs-tools`(Ubuntu, Debian) / `btrfs-progs-devel`(Fedora, CentOS, RHEL) -2. Install **`pkg-config`** (required for linking with `libseccomp`). -3. Install and setup a Go 1.13.15 development environment. -4. Make a local clone of this repository. -5. Install binary dependencies by running the following command from your cloned `cri/` project directory: -```bash -# Note: install.deps installs the above mentioned runc, containerd, and CNI -# binary dependencies. install.deps is only provided for general use and ease of -# testing. To customize `runc` and `containerd` build tags and/or to configure -# `cni`, please follow instructions in their documents. -make install.deps -``` -### Build and Install `cri` -To build and install a version of containerd with the `cri` plugin, enter the -following commands from your `cri` project directory: -```bash -make -sudo make install -``` -*NOTE: The version of containerd built and installed from the `Makefile` is only for -testing purposes. The version tag carries the suffix "-TEST".* -#### Build Tags -`cri` supports optional build tags for compiling support of various features. -To add build tags to the make option the `BUILD_TAGS` variable must be set. - -```bash -make BUILD_TAGS='seccomp apparmor selinux' -``` - -| Build Tag | Feature | Dependency | -|-----------|------------------------------------|---------------------------------| -| seccomp | syscall filtering | libseccomp development library | -| selinux | selinux process and mount labeling | | -| apparmor | apparmor profile support | | -### Validate Your `cri` Setup -A Kubernetes incubator project called [cri-tools](https://github.com/kubernetes-sigs/cri-tools) -includes programs for exercising CRI implementations such as the `cri` plugin. -More importantly, cri-tools includes the program `critest` which is used for running -[CRI Validation Testing](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-node/cri-validation.md). - -Run the CRI Validation test to validate your installation of `containerd` with `cri` built in: -```bash -make test-cri -``` -### Running a Kubernetes local cluster -If you already have a working development environment for supported Kubernetes -version, you can try `cri` in a local cluster: - -1. Start the version of `containerd` with `cri` plugin that you built and installed -above as root in a first terminal: -```bash -sudo containerd -``` -2. From the Kubernetes project directory startup a local cluster using `containerd`: -```bash -CONTAINER_RUNTIME=remote CONTAINER_RUNTIME_ENDPOINT='unix:///run/containerd/containerd.sock' ./hack/local-up-cluster.sh -``` -### Test -See [here](./docs/testing.md) for information about test. -## Using crictl -See [here](./docs/crictl.md) for information about using `crictl` to debug -pods, containers, and images. -## Configurations -See [here](./docs/config.md) for information about how to configure cri plugins -and [here](https://github.com/containerd/containerd/blob/master/docs/man/containerd-config.8.md) -for information about how to configure containerd -## Documentation -See [here](./docs) for additional documentation. -## Communication -For async communication and long running discussions please use issues and pull -requests on this github repo. This will be the best place to discuss design and -implementation. - -For sync communication catch us in the `#containerd` and `#containerd-dev` slack -channels on Cloud Native Computing Foundation's (CNCF) slack - -`cloud-native.slack.com`. Everyone is welcome to join and chat. -[Get Invite to CNCF slack.](https://slack.cncf.io) - -## Other Communications -As this project is tightly coupled to CRI and CRI-Tools and they are Kubernetes -projects, some of our project communications take place in the Kubernetes' SIG: -`sig-node.` - -For more information about `sig-node`, `CRI`, and the `CRI-Tools` projects: -* [sig-node community site](https://github.com/kubernetes/community/tree/master/sig-node) -* Slack: `#sig-node` channel in Kubernetes (kubernetes.slack.com) -* Mailing List: https://groups.google.com/forum/#!forum/kubernetes-sig-node - -### Reporting Security Issues - -__If you are reporting a security issue, please reach out discreetly at security@containerd.io__. - -## Licenses -The containerd codebase is released under the [Apache 2.0 license](https://github.com/containerd/containerd/blob/master/LICENSE.code). -The README.md file, and files in the "docs" folder are licensed under the -Creative Commons Attribution 4.0 International License under the terms and -conditions set forth in the file "[LICENSE.docs](https://github.com/containerd/containerd/blob/master/LICENSE.docs)". You may obtain a duplicate -copy of the same license, titled CC-BY-4.0, at http://creativecommons.org/licenses/by/4.0/. - -## Project details -cri is a containerd sub-project. This project was originally established in -April of 2017 in the Kubernetes Incubator program. After reaching the Beta -stage, In January of 2018, the project was merged into [containerd](https://github.com/containerd/containerd). -As a containerd sub-project, you will find the: -* [Project governance](https://github.com/containerd/project/blob/master/GOVERNANCE.md), -* [Maintainers](https://github.com/containerd/project/blob/master/MAINTAINERS), -* and [Contributing guidelines](https://github.com/containerd/project/blob/master/CONTRIBUTING.md) - -information in our [`containerd/project`](https://github.com/containerd/project) repository. diff --git a/vendor/github.com/containerd/cri/vendor.conf b/vendor/github.com/containerd/cri/vendor.conf deleted file mode 100644 index 82139978f..000000000 --- a/vendor/github.com/containerd/cri/vendor.conf +++ /dev/null @@ -1,102 +0,0 @@ -# cri dependencies -github.com/docker/docker 4634ce647cf2ce2c6031129ccd109e557244986f -github.com/opencontainers/selinux v1.6.0 -github.com/tchap/go-patricia v2.2.6 -github.com/willf/bitset v1.1.11 - -# containerd dependencies -github.com/beorn7/perks v1.0.1 -github.com/BurntSushi/toml v0.3.1 -github.com/cespare/xxhash/v2 v2.1.1 -github.com/containerd/cgroups 318312a373405e5e91134d8063d04d59768a1bff -github.com/containerd/console v1.0.0 -github.com/containerd/containerd v1.4.0 -github.com/containerd/continuity efbc4488d8fe1bdc16bde3b2d2990d9b3a899165 -github.com/containerd/fifo f15a3290365b9d2627d189e619ab4008e0069caf -github.com/containerd/go-runc 7016d3ce2328dd2cb1192b2076ebd565c4e8df0c -github.com/containerd/nri 0afc7f031eaf9c7d9c1a381b7ab5462e89c998fc -github.com/containerd/ttrpc v1.0.1 -github.com/containerd/typeurl v1.0.1 -github.com/coreos/go-systemd/v22 v22.1.0 -github.com/cpuguy83/go-md2man/v2 v2.0.0 -github.com/docker/go-events e31b211e4f1cd09aa76fe4ac244571fab96ae47f -github.com/docker/go-metrics v0.0.1 -github.com/docker/go-units v0.4.0 -github.com/godbus/dbus/v5 v5.0.3 -github.com/gogo/googleapis v1.3.2 -github.com/gogo/protobuf v1.3.1 -github.com/golang/protobuf v1.3.5 -github.com/google/uuid v1.1.1 -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 -github.com/hashicorp/errwrap v1.0.0 -github.com/hashicorp/go-multierror v1.0.0 -github.com/hashicorp/golang-lru v0.5.3 -github.com/imdario/mergo v0.3.7 -github.com/konsorten/go-windows-terminal-sequences v1.0.3 -github.com/matttproud/golang_protobuf_extensions v1.0.1 -github.com/Microsoft/go-winio v0.4.14 -github.com/Microsoft/hcsshim v0.8.9 -github.com/opencontainers/go-digest v1.0.0 -github.com/opencontainers/image-spec v1.0.1 -github.com/opencontainers/runc v1.0.0-rc92 -github.com/opencontainers/runtime-spec 4d89ac9fbff6c455f46a5bb59c6b1bb7184a5e43 # v1.0.3-0.20200728170252-4d89ac9fbff6 -github.com/pkg/errors v0.9.1 -github.com/prometheus/client_golang v1.6.0 -github.com/prometheus/client_model v0.2.0 -github.com/prometheus/common v0.9.1 -github.com/prometheus/procfs v0.0.11 -github.com/russross/blackfriday/v2 v2.0.1 -github.com/shurcooL/sanitized_anchor_name v1.0.0 -github.com/sirupsen/logrus v1.6.0 -github.com/syndtr/gocapability d98352740cb2c55f81556b63d4a1ec64c5a319c2 -github.com/urfave/cli v1.22.1 # NOTE: urfave/cli must be <= v1.22.1 due to a regression: https://github.com/urfave/cli/issues/1092 -go.etcd.io/bbolt v1.3.5 -go.opencensus.io v0.22.0 -golang.org/x/net ab34263943818b32f575efc978a3d24e80b04bd7 -golang.org/x/sync 42b317875d0fa942474b76e1b46a6060d720ae6e -golang.org/x/sys ed371f2e16b4b305ee99df548828de367527b76b -golang.org/x/text v0.3.3 -google.golang.org/genproto e50cd9704f63023d62cd06a1994b98227fc4d21a -google.golang.org/grpc v1.27.1 - -# cgroups dependencies -github.com/cilium/ebpf 1c8d4c9ef7759622653a1d319284a44652333b28 - -# kubernetes dependencies -github.com/davecgh/go-spew v1.1.1 -github.com/docker/spdystream 449fdfce4d962303d702fec724ef0ad181c92528 -github.com/emicklei/go-restful v2.9.5 -github.com/go-logr/logr v0.2.0 -github.com/google/gofuzz v1.1.0 -github.com/json-iterator/go v1.1.10 -github.com/modern-go/concurrent 1.0.3 -github.com/modern-go/reflect2 v1.0.1 -github.com/pmezard/go-difflib v1.0.0 -github.com/stretchr/testify v1.4.0 -golang.org/x/crypto 75b288015ac94e66e3d6715fb68a9b41bf046ec2 -golang.org/x/oauth2 858c2ad4c8b6c5d10852cb89079f6ca1c7309787 -golang.org/x/time 555d28b269f0569763d25dbe1a237ae74c6bcc82 -gopkg.in/inf.v0 v0.9.1 -gopkg.in/yaml.v2 v2.2.8 -k8s.io/api v0.19.0-rc.4 -k8s.io/apiserver v0.19.0-rc.4 -k8s.io/apimachinery v0.19.0-rc.4 -k8s.io/client-go v0.19.0-rc.4 -k8s.io/component-base v0.19.0-rc.4 -k8s.io/cri-api v0.19.0-rc.4 -k8s.io/klog/v2 v2.2.0 -k8s.io/utils 2df71ebbae66f39338aed4cd0bb82d2212ee33cc -sigs.k8s.io/structured-merge-diff/v3 v3.0.0 -sigs.k8s.io/yaml v1.2.0 - -# cni dependencies -github.com/containerd/go-cni v1.0.1 -github.com/containernetworking/cni v0.8.0 -github.com/containernetworking/plugins v0.8.6 -github.com/fsnotify/fsnotify v1.4.9 - -# image decrypt depedencies -github.com/containerd/imgcrypt v1.0.1 -github.com/containers/ocicrypt v1.0.1 -github.com/fullsailor/pkcs7 8306686428a5fe132eac8cb7c4848af725098bd4 -gopkg.in/square/go-jose.v2 v2.3.1 diff --git a/vendor/github.com/pmezard/go-difflib/LICENSE b/vendor/github.com/pmezard/go-difflib/LICENSE new file mode 100644 index 000000000..c67dad612 --- /dev/null +++ b/vendor/github.com/pmezard/go-difflib/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2013, Patrick Mezard +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + Redistributions in binary form must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the +documentation and/or other materials provided with the distribution. + The names of its contributors may not be used to endorse or promote +products derived from this software without specific prior written +permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/pmezard/go-difflib/README.md b/vendor/github.com/pmezard/go-difflib/README.md new file mode 100644 index 000000000..e87f307ed --- /dev/null +++ b/vendor/github.com/pmezard/go-difflib/README.md @@ -0,0 +1,50 @@ +go-difflib +========== + +[![Build Status](https://travis-ci.org/pmezard/go-difflib.png?branch=master)](https://travis-ci.org/pmezard/go-difflib) +[![GoDoc](https://godoc.org/github.com/pmezard/go-difflib/difflib?status.svg)](https://godoc.org/github.com/pmezard/go-difflib/difflib) + +Go-difflib is a partial port of python 3 difflib package. Its main goal +was to make unified and context diff available in pure Go, mostly for +testing purposes. + +The following class and functions (and related tests) have be ported: + +* `SequenceMatcher` +* `unified_diff()` +* `context_diff()` + +## Installation + +```bash +$ go get github.com/pmezard/go-difflib/difflib +``` + +### Quick Start + +Diffs are configured with Unified (or ContextDiff) structures, and can +be output to an io.Writer or returned as a string. + +```Go +diff := UnifiedDiff{ + A: difflib.SplitLines("foo\nbar\n"), + B: difflib.SplitLines("foo\nbaz\n"), + FromFile: "Original", + ToFile: "Current", + Context: 3, +} +text, _ := GetUnifiedDiffString(diff) +fmt.Printf(text) +``` + +would output: + +``` +--- Original ++++ Current +@@ -1,3 +1,3 @@ + foo +-bar ++baz +``` + diff --git a/vendor/github.com/pmezard/go-difflib/difflib/difflib.go b/vendor/github.com/pmezard/go-difflib/difflib/difflib.go new file mode 100644 index 000000000..003e99fad --- /dev/null +++ b/vendor/github.com/pmezard/go-difflib/difflib/difflib.go @@ -0,0 +1,772 @@ +// Package difflib is a partial port of Python difflib module. +// +// It provides tools to compare sequences of strings and generate textual diffs. +// +// The following class and functions have been ported: +// +// - SequenceMatcher +// +// - unified_diff +// +// - context_diff +// +// Getting unified diffs was the main goal of the port. Keep in mind this code +// is mostly suitable to output text differences in a human friendly way, there +// are no guarantees generated diffs are consumable by patch(1). +package difflib + +import ( + "bufio" + "bytes" + "fmt" + "io" + "strings" +) + +func min(a, b int) int { + if a < b { + return a + } + return b +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} + +func calculateRatio(matches, length int) float64 { + if length > 0 { + return 2.0 * float64(matches) / float64(length) + } + return 1.0 +} + +type Match struct { + A int + B int + Size int +} + +type OpCode struct { + Tag byte + I1 int + I2 int + J1 int + J2 int +} + +// SequenceMatcher compares sequence of strings. The basic +// algorithm predates, and is a little fancier than, an algorithm +// published in the late 1980's by Ratcliff and Obershelp under the +// hyperbolic name "gestalt pattern matching". The basic idea is to find +// the longest contiguous matching subsequence that contains no "junk" +// elements (R-O doesn't address junk). The same idea is then applied +// recursively to the pieces of the sequences to the left and to the right +// of the matching subsequence. This does not yield minimal edit +// sequences, but does tend to yield matches that "look right" to people. +// +// SequenceMatcher tries to compute a "human-friendly diff" between two +// sequences. Unlike e.g. UNIX(tm) diff, the fundamental notion is the +// longest *contiguous* & junk-free matching subsequence. That's what +// catches peoples' eyes. The Windows(tm) windiff has another interesting +// notion, pairing up elements that appear uniquely in each sequence. +// That, and the method here, appear to yield more intuitive difference +// reports than does diff. This method appears to be the least vulnerable +// to synching up on blocks of "junk lines", though (like blank lines in +// ordinary text files, or maybe "

" lines in HTML files). That may be +// because this is the only method of the 3 that has a *concept* of +// "junk" . +// +// Timing: Basic R-O is cubic time worst case and quadratic time expected +// case. SequenceMatcher is quadratic time for the worst case and has +// expected-case behavior dependent in a complicated way on how many +// elements the sequences have in common; best case time is linear. +type SequenceMatcher struct { + a []string + b []string + b2j map[string][]int + IsJunk func(string) bool + autoJunk bool + bJunk map[string]struct{} + matchingBlocks []Match + fullBCount map[string]int + bPopular map[string]struct{} + opCodes []OpCode +} + +func NewMatcher(a, b []string) *SequenceMatcher { + m := SequenceMatcher{autoJunk: true} + m.SetSeqs(a, b) + return &m +} + +func NewMatcherWithJunk(a, b []string, autoJunk bool, + isJunk func(string) bool) *SequenceMatcher { + + m := SequenceMatcher{IsJunk: isJunk, autoJunk: autoJunk} + m.SetSeqs(a, b) + return &m +} + +// Set two sequences to be compared. +func (m *SequenceMatcher) SetSeqs(a, b []string) { + m.SetSeq1(a) + m.SetSeq2(b) +} + +// Set the first sequence to be compared. The second sequence to be compared is +// not changed. +// +// SequenceMatcher computes and caches detailed information about the second +// sequence, so if you want to compare one sequence S against many sequences, +// use .SetSeq2(s) once and call .SetSeq1(x) repeatedly for each of the other +// sequences. +// +// See also SetSeqs() and SetSeq2(). +func (m *SequenceMatcher) SetSeq1(a []string) { + if &a == &m.a { + return + } + m.a = a + m.matchingBlocks = nil + m.opCodes = nil +} + +// Set the second sequence to be compared. The first sequence to be compared is +// not changed. +func (m *SequenceMatcher) SetSeq2(b []string) { + if &b == &m.b { + return + } + m.b = b + m.matchingBlocks = nil + m.opCodes = nil + m.fullBCount = nil + m.chainB() +} + +func (m *SequenceMatcher) chainB() { + // Populate line -> index mapping + b2j := map[string][]int{} + for i, s := range m.b { + indices := b2j[s] + indices = append(indices, i) + b2j[s] = indices + } + + // Purge junk elements + m.bJunk = map[string]struct{}{} + if m.IsJunk != nil { + junk := m.bJunk + for s, _ := range b2j { + if m.IsJunk(s) { + junk[s] = struct{}{} + } + } + for s, _ := range junk { + delete(b2j, s) + } + } + + // Purge remaining popular elements + popular := map[string]struct{}{} + n := len(m.b) + if m.autoJunk && n >= 200 { + ntest := n/100 + 1 + for s, indices := range b2j { + if len(indices) > ntest { + popular[s] = struct{}{} + } + } + for s, _ := range popular { + delete(b2j, s) + } + } + m.bPopular = popular + m.b2j = b2j +} + +func (m *SequenceMatcher) isBJunk(s string) bool { + _, ok := m.bJunk[s] + return ok +} + +// Find longest matching block in a[alo:ahi] and b[blo:bhi]. +// +// If IsJunk is not defined: +// +// Return (i,j,k) such that a[i:i+k] is equal to b[j:j+k], where +// alo <= i <= i+k <= ahi +// blo <= j <= j+k <= bhi +// and for all (i',j',k') meeting those conditions, +// k >= k' +// i <= i' +// and if i == i', j <= j' +// +// In other words, of all maximal matching blocks, return one that +// starts earliest in a, and of all those maximal matching blocks that +// start earliest in a, return the one that starts earliest in b. +// +// If IsJunk is defined, first the longest matching block is +// determined as above, but with the additional restriction that no +// junk element appears in the block. Then that block is extended as +// far as possible by matching (only) junk elements on both sides. So +// the resulting block never matches on junk except as identical junk +// happens to be adjacent to an "interesting" match. +// +// If no blocks match, return (alo, blo, 0). +func (m *SequenceMatcher) findLongestMatch(alo, ahi, blo, bhi int) Match { + // CAUTION: stripping common prefix or suffix would be incorrect. + // E.g., + // ab + // acab + // Longest matching block is "ab", but if common prefix is + // stripped, it's "a" (tied with "b"). UNIX(tm) diff does so + // strip, so ends up claiming that ab is changed to acab by + // inserting "ca" in the middle. That's minimal but unintuitive: + // "it's obvious" that someone inserted "ac" at the front. + // Windiff ends up at the same place as diff, but by pairing up + // the unique 'b's and then matching the first two 'a's. + besti, bestj, bestsize := alo, blo, 0 + + // find longest junk-free match + // during an iteration of the loop, j2len[j] = length of longest + // junk-free match ending with a[i-1] and b[j] + j2len := map[int]int{} + for i := alo; i != ahi; i++ { + // look at all instances of a[i] in b; note that because + // b2j has no junk keys, the loop is skipped if a[i] is junk + newj2len := map[int]int{} + for _, j := range m.b2j[m.a[i]] { + // a[i] matches b[j] + if j < blo { + continue + } + if j >= bhi { + break + } + k := j2len[j-1] + 1 + newj2len[j] = k + if k > bestsize { + besti, bestj, bestsize = i-k+1, j-k+1, k + } + } + j2len = newj2len + } + + // Extend the best by non-junk elements on each end. In particular, + // "popular" non-junk elements aren't in b2j, which greatly speeds + // the inner loop above, but also means "the best" match so far + // doesn't contain any junk *or* popular non-junk elements. + for besti > alo && bestj > blo && !m.isBJunk(m.b[bestj-1]) && + m.a[besti-1] == m.b[bestj-1] { + besti, bestj, bestsize = besti-1, bestj-1, bestsize+1 + } + for besti+bestsize < ahi && bestj+bestsize < bhi && + !m.isBJunk(m.b[bestj+bestsize]) && + m.a[besti+bestsize] == m.b[bestj+bestsize] { + bestsize += 1 + } + + // Now that we have a wholly interesting match (albeit possibly + // empty!), we may as well suck up the matching junk on each + // side of it too. Can't think of a good reason not to, and it + // saves post-processing the (possibly considerable) expense of + // figuring out what to do with it. In the case of an empty + // interesting match, this is clearly the right thing to do, + // because no other kind of match is possible in the regions. + for besti > alo && bestj > blo && m.isBJunk(m.b[bestj-1]) && + m.a[besti-1] == m.b[bestj-1] { + besti, bestj, bestsize = besti-1, bestj-1, bestsize+1 + } + for besti+bestsize < ahi && bestj+bestsize < bhi && + m.isBJunk(m.b[bestj+bestsize]) && + m.a[besti+bestsize] == m.b[bestj+bestsize] { + bestsize += 1 + } + + return Match{A: besti, B: bestj, Size: bestsize} +} + +// Return list of triples describing matching subsequences. +// +// Each triple is of the form (i, j, n), and means that +// a[i:i+n] == b[j:j+n]. The triples are monotonically increasing in +// i and in j. It's also guaranteed that if (i, j, n) and (i', j', n') are +// adjacent triples in the list, and the second is not the last triple in the +// list, then i+n != i' or j+n != j'. IOW, adjacent triples never describe +// adjacent equal blocks. +// +// The last triple is a dummy, (len(a), len(b), 0), and is the only +// triple with n==0. +func (m *SequenceMatcher) GetMatchingBlocks() []Match { + if m.matchingBlocks != nil { + return m.matchingBlocks + } + + var matchBlocks func(alo, ahi, blo, bhi int, matched []Match) []Match + matchBlocks = func(alo, ahi, blo, bhi int, matched []Match) []Match { + match := m.findLongestMatch(alo, ahi, blo, bhi) + i, j, k := match.A, match.B, match.Size + if match.Size > 0 { + if alo < i && blo < j { + matched = matchBlocks(alo, i, blo, j, matched) + } + matched = append(matched, match) + if i+k < ahi && j+k < bhi { + matched = matchBlocks(i+k, ahi, j+k, bhi, matched) + } + } + return matched + } + matched := matchBlocks(0, len(m.a), 0, len(m.b), nil) + + // It's possible that we have adjacent equal blocks in the + // matching_blocks list now. + nonAdjacent := []Match{} + i1, j1, k1 := 0, 0, 0 + for _, b := range matched { + // Is this block adjacent to i1, j1, k1? + i2, j2, k2 := b.A, b.B, b.Size + if i1+k1 == i2 && j1+k1 == j2 { + // Yes, so collapse them -- this just increases the length of + // the first block by the length of the second, and the first + // block so lengthened remains the block to compare against. + k1 += k2 + } else { + // Not adjacent. Remember the first block (k1==0 means it's + // the dummy we started with), and make the second block the + // new block to compare against. + if k1 > 0 { + nonAdjacent = append(nonAdjacent, Match{i1, j1, k1}) + } + i1, j1, k1 = i2, j2, k2 + } + } + if k1 > 0 { + nonAdjacent = append(nonAdjacent, Match{i1, j1, k1}) + } + + nonAdjacent = append(nonAdjacent, Match{len(m.a), len(m.b), 0}) + m.matchingBlocks = nonAdjacent + return m.matchingBlocks +} + +// Return list of 5-tuples describing how to turn a into b. +// +// Each tuple is of the form (tag, i1, i2, j1, j2). The first tuple +// has i1 == j1 == 0, and remaining tuples have i1 == the i2 from the +// tuple preceding it, and likewise for j1 == the previous j2. +// +// The tags are characters, with these meanings: +// +// 'r' (replace): a[i1:i2] should be replaced by b[j1:j2] +// +// 'd' (delete): a[i1:i2] should be deleted, j1==j2 in this case. +// +// 'i' (insert): b[j1:j2] should be inserted at a[i1:i1], i1==i2 in this case. +// +// 'e' (equal): a[i1:i2] == b[j1:j2] +func (m *SequenceMatcher) GetOpCodes() []OpCode { + if m.opCodes != nil { + return m.opCodes + } + i, j := 0, 0 + matching := m.GetMatchingBlocks() + opCodes := make([]OpCode, 0, len(matching)) + for _, m := range matching { + // invariant: we've pumped out correct diffs to change + // a[:i] into b[:j], and the next matching block is + // a[ai:ai+size] == b[bj:bj+size]. So we need to pump + // out a diff to change a[i:ai] into b[j:bj], pump out + // the matching block, and move (i,j) beyond the match + ai, bj, size := m.A, m.B, m.Size + tag := byte(0) + if i < ai && j < bj { + tag = 'r' + } else if i < ai { + tag = 'd' + } else if j < bj { + tag = 'i' + } + if tag > 0 { + opCodes = append(opCodes, OpCode{tag, i, ai, j, bj}) + } + i, j = ai+size, bj+size + // the list of matching blocks is terminated by a + // sentinel with size 0 + if size > 0 { + opCodes = append(opCodes, OpCode{'e', ai, i, bj, j}) + } + } + m.opCodes = opCodes + return m.opCodes +} + +// Isolate change clusters by eliminating ranges with no changes. +// +// Return a generator of groups with up to n lines of context. +// Each group is in the same format as returned by GetOpCodes(). +func (m *SequenceMatcher) GetGroupedOpCodes(n int) [][]OpCode { + if n < 0 { + n = 3 + } + codes := m.GetOpCodes() + if len(codes) == 0 { + codes = []OpCode{OpCode{'e', 0, 1, 0, 1}} + } + // Fixup leading and trailing groups if they show no changes. + if codes[0].Tag == 'e' { + c := codes[0] + i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2 + codes[0] = OpCode{c.Tag, max(i1, i2-n), i2, max(j1, j2-n), j2} + } + if codes[len(codes)-1].Tag == 'e' { + c := codes[len(codes)-1] + i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2 + codes[len(codes)-1] = OpCode{c.Tag, i1, min(i2, i1+n), j1, min(j2, j1+n)} + } + nn := n + n + groups := [][]OpCode{} + group := []OpCode{} + for _, c := range codes { + i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2 + // End the current group and start a new one whenever + // there is a large range with no changes. + if c.Tag == 'e' && i2-i1 > nn { + group = append(group, OpCode{c.Tag, i1, min(i2, i1+n), + j1, min(j2, j1+n)}) + groups = append(groups, group) + group = []OpCode{} + i1, j1 = max(i1, i2-n), max(j1, j2-n) + } + group = append(group, OpCode{c.Tag, i1, i2, j1, j2}) + } + if len(group) > 0 && !(len(group) == 1 && group[0].Tag == 'e') { + groups = append(groups, group) + } + return groups +} + +// Return a measure of the sequences' similarity (float in [0,1]). +// +// Where T is the total number of elements in both sequences, and +// M is the number of matches, this is 2.0*M / T. +// Note that this is 1 if the sequences are identical, and 0 if +// they have nothing in common. +// +// .Ratio() is expensive to compute if you haven't already computed +// .GetMatchingBlocks() or .GetOpCodes(), in which case you may +// want to try .QuickRatio() or .RealQuickRation() first to get an +// upper bound. +func (m *SequenceMatcher) Ratio() float64 { + matches := 0 + for _, m := range m.GetMatchingBlocks() { + matches += m.Size + } + return calculateRatio(matches, len(m.a)+len(m.b)) +} + +// Return an upper bound on ratio() relatively quickly. +// +// This isn't defined beyond that it is an upper bound on .Ratio(), and +// is faster to compute. +func (m *SequenceMatcher) QuickRatio() float64 { + // viewing a and b as multisets, set matches to the cardinality + // of their intersection; this counts the number of matches + // without regard to order, so is clearly an upper bound + if m.fullBCount == nil { + m.fullBCount = map[string]int{} + for _, s := range m.b { + m.fullBCount[s] = m.fullBCount[s] + 1 + } + } + + // avail[x] is the number of times x appears in 'b' less the + // number of times we've seen it in 'a' so far ... kinda + avail := map[string]int{} + matches := 0 + for _, s := range m.a { + n, ok := avail[s] + if !ok { + n = m.fullBCount[s] + } + avail[s] = n - 1 + if n > 0 { + matches += 1 + } + } + return calculateRatio(matches, len(m.a)+len(m.b)) +} + +// Return an upper bound on ratio() very quickly. +// +// This isn't defined beyond that it is an upper bound on .Ratio(), and +// is faster to compute than either .Ratio() or .QuickRatio(). +func (m *SequenceMatcher) RealQuickRatio() float64 { + la, lb := len(m.a), len(m.b) + return calculateRatio(min(la, lb), la+lb) +} + +// Convert range to the "ed" format +func formatRangeUnified(start, stop int) string { + // Per the diff spec at http://www.unix.org/single_unix_specification/ + beginning := start + 1 // lines start numbering with one + length := stop - start + if length == 1 { + return fmt.Sprintf("%d", beginning) + } + if length == 0 { + beginning -= 1 // empty ranges begin at line just before the range + } + return fmt.Sprintf("%d,%d", beginning, length) +} + +// Unified diff parameters +type UnifiedDiff struct { + A []string // First sequence lines + FromFile string // First file name + FromDate string // First file time + B []string // Second sequence lines + ToFile string // Second file name + ToDate string // Second file time + Eol string // Headers end of line, defaults to LF + Context int // Number of context lines +} + +// Compare two sequences of lines; generate the delta as a unified diff. +// +// Unified diffs are a compact way of showing line changes and a few +// lines of context. The number of context lines is set by 'n' which +// defaults to three. +// +// By default, the diff control lines (those with ---, +++, or @@) are +// created with a trailing newline. This is helpful so that inputs +// created from file.readlines() result in diffs that are suitable for +// file.writelines() since both the inputs and outputs have trailing +// newlines. +// +// For inputs that do not have trailing newlines, set the lineterm +// argument to "" so that the output will be uniformly newline free. +// +// The unidiff format normally has a header for filenames and modification +// times. Any or all of these may be specified using strings for +// 'fromfile', 'tofile', 'fromfiledate', and 'tofiledate'. +// The modification times are normally expressed in the ISO 8601 format. +func WriteUnifiedDiff(writer io.Writer, diff UnifiedDiff) error { + buf := bufio.NewWriter(writer) + defer buf.Flush() + wf := func(format string, args ...interface{}) error { + _, err := buf.WriteString(fmt.Sprintf(format, args...)) + return err + } + ws := func(s string) error { + _, err := buf.WriteString(s) + return err + } + + if len(diff.Eol) == 0 { + diff.Eol = "\n" + } + + started := false + m := NewMatcher(diff.A, diff.B) + for _, g := range m.GetGroupedOpCodes(diff.Context) { + if !started { + started = true + fromDate := "" + if len(diff.FromDate) > 0 { + fromDate = "\t" + diff.FromDate + } + toDate := "" + if len(diff.ToDate) > 0 { + toDate = "\t" + diff.ToDate + } + if diff.FromFile != "" || diff.ToFile != "" { + err := wf("--- %s%s%s", diff.FromFile, fromDate, diff.Eol) + if err != nil { + return err + } + err = wf("+++ %s%s%s", diff.ToFile, toDate, diff.Eol) + if err != nil { + return err + } + } + } + first, last := g[0], g[len(g)-1] + range1 := formatRangeUnified(first.I1, last.I2) + range2 := formatRangeUnified(first.J1, last.J2) + if err := wf("@@ -%s +%s @@%s", range1, range2, diff.Eol); err != nil { + return err + } + for _, c := range g { + i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2 + if c.Tag == 'e' { + for _, line := range diff.A[i1:i2] { + if err := ws(" " + line); err != nil { + return err + } + } + continue + } + if c.Tag == 'r' || c.Tag == 'd' { + for _, line := range diff.A[i1:i2] { + if err := ws("-" + line); err != nil { + return err + } + } + } + if c.Tag == 'r' || c.Tag == 'i' { + for _, line := range diff.B[j1:j2] { + if err := ws("+" + line); err != nil { + return err + } + } + } + } + } + return nil +} + +// Like WriteUnifiedDiff but returns the diff a string. +func GetUnifiedDiffString(diff UnifiedDiff) (string, error) { + w := &bytes.Buffer{} + err := WriteUnifiedDiff(w, diff) + return string(w.Bytes()), err +} + +// Convert range to the "ed" format. +func formatRangeContext(start, stop int) string { + // Per the diff spec at http://www.unix.org/single_unix_specification/ + beginning := start + 1 // lines start numbering with one + length := stop - start + if length == 0 { + beginning -= 1 // empty ranges begin at line just before the range + } + if length <= 1 { + return fmt.Sprintf("%d", beginning) + } + return fmt.Sprintf("%d,%d", beginning, beginning+length-1) +} + +type ContextDiff UnifiedDiff + +// Compare two sequences of lines; generate the delta as a context diff. +// +// Context diffs are a compact way of showing line changes and a few +// lines of context. The number of context lines is set by diff.Context +// which defaults to three. +// +// By default, the diff control lines (those with *** or ---) are +// created with a trailing newline. +// +// For inputs that do not have trailing newlines, set the diff.Eol +// argument to "" so that the output will be uniformly newline free. +// +// The context diff format normally has a header for filenames and +// modification times. Any or all of these may be specified using +// strings for diff.FromFile, diff.ToFile, diff.FromDate, diff.ToDate. +// The modification times are normally expressed in the ISO 8601 format. +// If not specified, the strings default to blanks. +func WriteContextDiff(writer io.Writer, diff ContextDiff) error { + buf := bufio.NewWriter(writer) + defer buf.Flush() + var diffErr error + wf := func(format string, args ...interface{}) { + _, err := buf.WriteString(fmt.Sprintf(format, args...)) + if diffErr == nil && err != nil { + diffErr = err + } + } + ws := func(s string) { + _, err := buf.WriteString(s) + if diffErr == nil && err != nil { + diffErr = err + } + } + + if len(diff.Eol) == 0 { + diff.Eol = "\n" + } + + prefix := map[byte]string{ + 'i': "+ ", + 'd': "- ", + 'r': "! ", + 'e': " ", + } + + started := false + m := NewMatcher(diff.A, diff.B) + for _, g := range m.GetGroupedOpCodes(diff.Context) { + if !started { + started = true + fromDate := "" + if len(diff.FromDate) > 0 { + fromDate = "\t" + diff.FromDate + } + toDate := "" + if len(diff.ToDate) > 0 { + toDate = "\t" + diff.ToDate + } + if diff.FromFile != "" || diff.ToFile != "" { + wf("*** %s%s%s", diff.FromFile, fromDate, diff.Eol) + wf("--- %s%s%s", diff.ToFile, toDate, diff.Eol) + } + } + + first, last := g[0], g[len(g)-1] + ws("***************" + diff.Eol) + + range1 := formatRangeContext(first.I1, last.I2) + wf("*** %s ****%s", range1, diff.Eol) + for _, c := range g { + if c.Tag == 'r' || c.Tag == 'd' { + for _, cc := range g { + if cc.Tag == 'i' { + continue + } + for _, line := range diff.A[cc.I1:cc.I2] { + ws(prefix[cc.Tag] + line) + } + } + break + } + } + + range2 := formatRangeContext(first.J1, last.J2) + wf("--- %s ----%s", range2, diff.Eol) + for _, c := range g { + if c.Tag == 'r' || c.Tag == 'i' { + for _, cc := range g { + if cc.Tag == 'd' { + continue + } + for _, line := range diff.B[cc.J1:cc.J2] { + ws(prefix[cc.Tag] + line) + } + } + break + } + } + } + return diffErr +} + +// Like WriteContextDiff but returns the diff a string. +func GetContextDiffString(diff ContextDiff) (string, error) { + w := &bytes.Buffer{} + err := WriteContextDiff(w, diff) + return string(w.Bytes()), err +} + +// Split a string on "\n" while preserving them. The output can be used +// as input for UnifiedDiff and ContextDiff structures. +func SplitLines(s string) []string { + lines := strings.SplitAfter(s, "\n") + lines[len(lines)-1] += "\n" + return lines +} diff --git a/vendor/github.com/stretchr/testify/LICENSE b/vendor/github.com/stretchr/testify/LICENSE new file mode 100644 index 000000000..f38ec5956 --- /dev/null +++ b/vendor/github.com/stretchr/testify/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2012-2018 Mat Ryer and Tyler Bunnell + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/stretchr/testify/README.md b/vendor/github.com/stretchr/testify/README.md new file mode 100644 index 000000000..ef0197e8a --- /dev/null +++ b/vendor/github.com/stretchr/testify/README.md @@ -0,0 +1,342 @@ +Testify - Thou Shalt Write Tests +================================ + +[![Build Status](https://travis-ci.org/stretchr/testify.svg)](https://travis-ci.org/stretchr/testify) [![Go Report Card](https://goreportcard.com/badge/github.com/stretchr/testify)](https://goreportcard.com/report/github.com/stretchr/testify) [![GoDoc](https://godoc.org/github.com/stretchr/testify?status.svg)](https://godoc.org/github.com/stretchr/testify) + +Go code (golang) set of packages that provide many tools for testifying that your code will behave as you intend. + +Features include: + + * [Easy assertions](#assert-package) + * [Mocking](#mock-package) + * [Testing suite interfaces and functions](#suite-package) + +Get started: + + * Install testify with [one line of code](#installation), or [update it with another](#staying-up-to-date) + * For an introduction to writing test code in Go, see http://golang.org/doc/code.html#Testing + * Check out the API Documentation http://godoc.org/github.com/stretchr/testify + * To make your testing life easier, check out our other project, [gorc](http://github.com/stretchr/gorc) + * A little about [Test-Driven Development (TDD)](http://en.wikipedia.org/wiki/Test-driven_development) + + + +[`assert`](http://godoc.org/github.com/stretchr/testify/assert "API documentation") package +------------------------------------------------------------------------------------------- + +The `assert` package provides some helpful methods that allow you to write better test code in Go. + + * Prints friendly, easy to read failure descriptions + * Allows for very readable code + * Optionally annotate each assertion with a message + +See it in action: + +```go +package yours + +import ( + "testing" + "github.com/stretchr/testify/assert" +) + +func TestSomething(t *testing.T) { + + // assert equality + assert.Equal(t, 123, 123, "they should be equal") + + // assert inequality + assert.NotEqual(t, 123, 456, "they should not be equal") + + // assert for nil (good for errors) + assert.Nil(t, object) + + // assert for not nil (good when you expect something) + if assert.NotNil(t, object) { + + // now we know that object isn't nil, we are safe to make + // further assertions without causing any errors + assert.Equal(t, "Something", object.Value) + + } + +} +``` + + * Every assert func takes the `testing.T` object as the first argument. This is how it writes the errors out through the normal `go test` capabilities. + * Every assert func returns a bool indicating whether the assertion was successful or not, this is useful for if you want to go on making further assertions under certain conditions. + +if you assert many times, use the below: + +```go +package yours + +import ( + "testing" + "github.com/stretchr/testify/assert" +) + +func TestSomething(t *testing.T) { + assert := assert.New(t) + + // assert equality + assert.Equal(123, 123, "they should be equal") + + // assert inequality + assert.NotEqual(123, 456, "they should not be equal") + + // assert for nil (good for errors) + assert.Nil(object) + + // assert for not nil (good when you expect something) + if assert.NotNil(object) { + + // now we know that object isn't nil, we are safe to make + // further assertions without causing any errors + assert.Equal("Something", object.Value) + } +} +``` + +[`require`](http://godoc.org/github.com/stretchr/testify/require "API documentation") package +--------------------------------------------------------------------------------------------- + +The `require` package provides same global functions as the `assert` package, but instead of returning a boolean result they terminate current test. + +See [t.FailNow](http://golang.org/pkg/testing/#T.FailNow) for details. + +[`mock`](http://godoc.org/github.com/stretchr/testify/mock "API documentation") package +---------------------------------------------------------------------------------------- + +The `mock` package provides a mechanism for easily writing mock objects that can be used in place of real objects when writing test code. + +An example test function that tests a piece of code that relies on an external object `testObj`, can setup expectations (testify) and assert that they indeed happened: + +```go +package yours + +import ( + "testing" + "github.com/stretchr/testify/mock" +) + +/* + Test objects +*/ + +// MyMockedObject is a mocked object that implements an interface +// that describes an object that the code I am testing relies on. +type MyMockedObject struct{ + mock.Mock +} + +// DoSomething is a method on MyMockedObject that implements some interface +// and just records the activity, and returns what the Mock object tells it to. +// +// In the real object, this method would do something useful, but since this +// is a mocked object - we're just going to stub it out. +// +// NOTE: This method is not being tested here, code that uses this object is. +func (m *MyMockedObject) DoSomething(number int) (bool, error) { + + args := m.Called(number) + return args.Bool(0), args.Error(1) + +} + +/* + Actual test functions +*/ + +// TestSomething is an example of how to use our test object to +// make assertions about some target code we are testing. +func TestSomething(t *testing.T) { + + // create an instance of our test object + testObj := new(MyMockedObject) + + // setup expectations + testObj.On("DoSomething", 123).Return(true, nil) + + // call the code we are testing + targetFuncThatDoesSomethingWithObj(testObj) + + // assert that the expectations were met + testObj.AssertExpectations(t) + + +} + +// TestSomethingElse is a second example of how to use our test object to +// make assertions about some target code we are testing. +// This time using a placeholder. Placeholders might be used when the +// data being passed in is normally dynamically generated and cannot be +// predicted beforehand (eg. containing hashes that are time sensitive) +func TestSomethingElse(t *testing.T) { + + // create an instance of our test object + testObj := new(MyMockedObject) + + // setup expectations with a placeholder in the argument list + testObj.On("DoSomething", mock.Anything).Return(true, nil) + + // call the code we are testing + targetFuncThatDoesSomethingWithObj(testObj) + + // assert that the expectations were met + testObj.AssertExpectations(t) + + +} +``` + +For more information on how to write mock code, check out the [API documentation for the `mock` package](http://godoc.org/github.com/stretchr/testify/mock). + +You can use the [mockery tool](http://github.com/vektra/mockery) to autogenerate the mock code against an interface as well, making using mocks much quicker. + +[`suite`](http://godoc.org/github.com/stretchr/testify/suite "API documentation") package +----------------------------------------------------------------------------------------- + +The `suite` package provides functionality that you might be used to from more common object oriented languages. With it, you can build a testing suite as a struct, build setup/teardown methods and testing methods on your struct, and run them with 'go test' as per normal. + +An example suite is shown below: + +```go +// Basic imports +import ( + "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/suite" +) + +// Define the suite, and absorb the built-in basic suite +// functionality from testify - including a T() method which +// returns the current testing context +type ExampleTestSuite struct { + suite.Suite + VariableThatShouldStartAtFive int +} + +// Make sure that VariableThatShouldStartAtFive is set to five +// before each test +func (suite *ExampleTestSuite) SetupTest() { + suite.VariableThatShouldStartAtFive = 5 +} + +// All methods that begin with "Test" are run as tests within a +// suite. +func (suite *ExampleTestSuite) TestExample() { + assert.Equal(suite.T(), 5, suite.VariableThatShouldStartAtFive) +} + +// In order for 'go test' to run this suite, we need to create +// a normal test function and pass our suite to suite.Run +func TestExampleTestSuite(t *testing.T) { + suite.Run(t, new(ExampleTestSuite)) +} +``` + +For a more complete example, using all of the functionality provided by the suite package, look at our [example testing suite](https://github.com/stretchr/testify/blob/master/suite/suite_test.go) + +For more information on writing suites, check out the [API documentation for the `suite` package](http://godoc.org/github.com/stretchr/testify/suite). + +`Suite` object has assertion methods: + +```go +// Basic imports +import ( + "testing" + "github.com/stretchr/testify/suite" +) + +// Define the suite, and absorb the built-in basic suite +// functionality from testify - including assertion methods. +type ExampleTestSuite struct { + suite.Suite + VariableThatShouldStartAtFive int +} + +// Make sure that VariableThatShouldStartAtFive is set to five +// before each test +func (suite *ExampleTestSuite) SetupTest() { + suite.VariableThatShouldStartAtFive = 5 +} + +// All methods that begin with "Test" are run as tests within a +// suite. +func (suite *ExampleTestSuite) TestExample() { + suite.Equal(suite.VariableThatShouldStartAtFive, 5) +} + +// In order for 'go test' to run this suite, we need to create +// a normal test function and pass our suite to suite.Run +func TestExampleTestSuite(t *testing.T) { + suite.Run(t, new(ExampleTestSuite)) +} +``` + +------ + +Installation +============ + +To install Testify, use `go get`: + + go get github.com/stretchr/testify + +This will then make the following packages available to you: + + github.com/stretchr/testify/assert + github.com/stretchr/testify/require + github.com/stretchr/testify/mock + github.com/stretchr/testify/suite + github.com/stretchr/testify/http (deprecated) + +Import the `testify/assert` package into your code using this template: + +```go +package yours + +import ( + "testing" + "github.com/stretchr/testify/assert" +) + +func TestSomething(t *testing.T) { + + assert.True(t, true, "True is true!") + +} +``` + +------ + +Staying up to date +================== + +To update Testify to the latest version, use `go get -u github.com/stretchr/testify`. + +------ + +Supported go versions +================== + +We support the three major Go versions, which are 1.9, 1.10, and 1.11 at the moment. + +------ + +Contributing +============ + +Please feel free to submit issues, fork the repository and send pull requests! + +When submitting an issue, we ask that you please include a complete test function that demonstrates the issue. Extra credit for those using Testify to write the test code that demonstrates it. + +Code generation is used. Look for `CODE GENERATED AUTOMATICALLY` at the top of some files. Run `go generate ./...` to update generated files. + +------ + +License +======= + +This project is licensed under the terms of the MIT license. diff --git a/vendor/github.com/stretchr/testify/assert/assertion_format.go b/vendor/github.com/stretchr/testify/assert/assertion_format.go new file mode 100644 index 000000000..e0364e9e7 --- /dev/null +++ b/vendor/github.com/stretchr/testify/assert/assertion_format.go @@ -0,0 +1,566 @@ +/* +* CODE GENERATED AUTOMATICALLY WITH github.com/stretchr/testify/_codegen +* THIS FILE MUST NOT BE EDITED BY HAND + */ + +package assert + +import ( + http "net/http" + url "net/url" + time "time" +) + +// Conditionf uses a Comparison to assert a complex condition. +func Conditionf(t TestingT, comp Comparison, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return Condition(t, comp, append([]interface{}{msg}, args...)...) +} + +// Containsf asserts that the specified string, list(array, slice...) or map contains the +// specified substring or element. +// +// assert.Containsf(t, "Hello World", "World", "error message %s", "formatted") +// assert.Containsf(t, ["Hello", "World"], "World", "error message %s", "formatted") +// assert.Containsf(t, {"Hello": "World"}, "Hello", "error message %s", "formatted") +func Containsf(t TestingT, s interface{}, contains interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return Contains(t, s, contains, append([]interface{}{msg}, args...)...) +} + +// DirExistsf checks whether a directory exists in the given path. It also fails if the path is a file rather a directory or there is an error checking whether it exists. +func DirExistsf(t TestingT, path string, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return DirExists(t, path, append([]interface{}{msg}, args...)...) +} + +// ElementsMatchf asserts that the specified listA(array, slice...) is equal to specified +// listB(array, slice...) ignoring the order of the elements. If there are duplicate elements, +// the number of appearances of each of them in both lists should match. +// +// assert.ElementsMatchf(t, [1, 3, 2, 3], [1, 3, 3, 2], "error message %s", "formatted") +func ElementsMatchf(t TestingT, listA interface{}, listB interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return ElementsMatch(t, listA, listB, append([]interface{}{msg}, args...)...) +} + +// Emptyf asserts that the specified object is empty. I.e. nil, "", false, 0 or either +// a slice or a channel with len == 0. +// +// assert.Emptyf(t, obj, "error message %s", "formatted") +func Emptyf(t TestingT, object interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return Empty(t, object, append([]interface{}{msg}, args...)...) +} + +// Equalf asserts that two objects are equal. +// +// assert.Equalf(t, 123, 123, "error message %s", "formatted") +// +// Pointer variable equality is determined based on the equality of the +// referenced values (as opposed to the memory addresses). Function equality +// cannot be determined and will always fail. +func Equalf(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return Equal(t, expected, actual, append([]interface{}{msg}, args...)...) +} + +// EqualErrorf asserts that a function returned an error (i.e. not `nil`) +// and that it is equal to the provided error. +// +// actualObj, err := SomeFunction() +// assert.EqualErrorf(t, err, expectedErrorString, "error message %s", "formatted") +func EqualErrorf(t TestingT, theError error, errString string, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return EqualError(t, theError, errString, append([]interface{}{msg}, args...)...) +} + +// EqualValuesf asserts that two objects are equal or convertable to the same types +// and equal. +// +// assert.EqualValuesf(t, uint32(123, "error message %s", "formatted"), int32(123)) +func EqualValuesf(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return EqualValues(t, expected, actual, append([]interface{}{msg}, args...)...) +} + +// Errorf asserts that a function returned an error (i.e. not `nil`). +// +// actualObj, err := SomeFunction() +// if assert.Errorf(t, err, "error message %s", "formatted") { +// assert.Equal(t, expectedErrorf, err) +// } +func Errorf(t TestingT, err error, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return Error(t, err, append([]interface{}{msg}, args...)...) +} + +// Eventuallyf asserts that given condition will be met in waitFor time, +// periodically checking target function each tick. +// +// assert.Eventuallyf(t, func() bool { return true; }, time.Second, 10*time.Millisecond, "error message %s", "formatted") +func Eventuallyf(t TestingT, condition func() bool, waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return Eventually(t, condition, waitFor, tick, append([]interface{}{msg}, args...)...) +} + +// Exactlyf asserts that two objects are equal in value and type. +// +// assert.Exactlyf(t, int32(123, "error message %s", "formatted"), int64(123)) +func Exactlyf(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return Exactly(t, expected, actual, append([]interface{}{msg}, args...)...) +} + +// Failf reports a failure through +func Failf(t TestingT, failureMessage string, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return Fail(t, failureMessage, append([]interface{}{msg}, args...)...) +} + +// FailNowf fails test +func FailNowf(t TestingT, failureMessage string, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return FailNow(t, failureMessage, append([]interface{}{msg}, args...)...) +} + +// Falsef asserts that the specified value is false. +// +// assert.Falsef(t, myBool, "error message %s", "formatted") +func Falsef(t TestingT, value bool, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return False(t, value, append([]interface{}{msg}, args...)...) +} + +// FileExistsf checks whether a file exists in the given path. It also fails if the path points to a directory or there is an error when trying to check the file. +func FileExistsf(t TestingT, path string, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return FileExists(t, path, append([]interface{}{msg}, args...)...) +} + +// Greaterf asserts that the first element is greater than the second +// +// assert.Greaterf(t, 2, 1, "error message %s", "formatted") +// assert.Greaterf(t, float64(2, "error message %s", "formatted"), float64(1)) +// assert.Greaterf(t, "b", "a", "error message %s", "formatted") +func Greaterf(t TestingT, e1 interface{}, e2 interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return Greater(t, e1, e2, append([]interface{}{msg}, args...)...) +} + +// GreaterOrEqualf asserts that the first element is greater than or equal to the second +// +// assert.GreaterOrEqualf(t, 2, 1, "error message %s", "formatted") +// assert.GreaterOrEqualf(t, 2, 2, "error message %s", "formatted") +// assert.GreaterOrEqualf(t, "b", "a", "error message %s", "formatted") +// assert.GreaterOrEqualf(t, "b", "b", "error message %s", "formatted") +func GreaterOrEqualf(t TestingT, e1 interface{}, e2 interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return GreaterOrEqual(t, e1, e2, append([]interface{}{msg}, args...)...) +} + +// HTTPBodyContainsf asserts that a specified handler returns a +// body that contains a string. +// +// assert.HTTPBodyContainsf(t, myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky", "error message %s", "formatted") +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPBodyContainsf(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return HTTPBodyContains(t, handler, method, url, values, str, append([]interface{}{msg}, args...)...) +} + +// HTTPBodyNotContainsf asserts that a specified handler returns a +// body that does not contain a string. +// +// assert.HTTPBodyNotContainsf(t, myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky", "error message %s", "formatted") +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPBodyNotContainsf(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return HTTPBodyNotContains(t, handler, method, url, values, str, append([]interface{}{msg}, args...)...) +} + +// HTTPErrorf asserts that a specified handler returns an error status code. +// +// assert.HTTPErrorf(t, myHandler, "POST", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// +// Returns whether the assertion was successful (true, "error message %s", "formatted") or not (false). +func HTTPErrorf(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return HTTPError(t, handler, method, url, values, append([]interface{}{msg}, args...)...) +} + +// HTTPRedirectf asserts that a specified handler returns a redirect status code. +// +// assert.HTTPRedirectf(t, myHandler, "GET", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// +// Returns whether the assertion was successful (true, "error message %s", "formatted") or not (false). +func HTTPRedirectf(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return HTTPRedirect(t, handler, method, url, values, append([]interface{}{msg}, args...)...) +} + +// HTTPSuccessf asserts that a specified handler returns a success status code. +// +// assert.HTTPSuccessf(t, myHandler, "POST", "http://www.google.com", nil, "error message %s", "formatted") +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPSuccessf(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return HTTPSuccess(t, handler, method, url, values, append([]interface{}{msg}, args...)...) +} + +// Implementsf asserts that an object is implemented by the specified interface. +// +// assert.Implementsf(t, (*MyInterface, "error message %s", "formatted")(nil), new(MyObject)) +func Implementsf(t TestingT, interfaceObject interface{}, object interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return Implements(t, interfaceObject, object, append([]interface{}{msg}, args...)...) +} + +// InDeltaf asserts that the two numerals are within delta of each other. +// +// assert.InDeltaf(t, math.Pi, (22 / 7.0, "error message %s", "formatted"), 0.01) +func InDeltaf(t TestingT, expected interface{}, actual interface{}, delta float64, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return InDelta(t, expected, actual, delta, append([]interface{}{msg}, args...)...) +} + +// InDeltaMapValuesf is the same as InDelta, but it compares all values between two maps. Both maps must have exactly the same keys. +func InDeltaMapValuesf(t TestingT, expected interface{}, actual interface{}, delta float64, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return InDeltaMapValues(t, expected, actual, delta, append([]interface{}{msg}, args...)...) +} + +// InDeltaSlicef is the same as InDelta, except it compares two slices. +func InDeltaSlicef(t TestingT, expected interface{}, actual interface{}, delta float64, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return InDeltaSlice(t, expected, actual, delta, append([]interface{}{msg}, args...)...) +} + +// InEpsilonf asserts that expected and actual have a relative error less than epsilon +func InEpsilonf(t TestingT, expected interface{}, actual interface{}, epsilon float64, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return InEpsilon(t, expected, actual, epsilon, append([]interface{}{msg}, args...)...) +} + +// InEpsilonSlicef is the same as InEpsilon, except it compares each value from two slices. +func InEpsilonSlicef(t TestingT, expected interface{}, actual interface{}, epsilon float64, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return InEpsilonSlice(t, expected, actual, epsilon, append([]interface{}{msg}, args...)...) +} + +// IsTypef asserts that the specified objects are of the same type. +func IsTypef(t TestingT, expectedType interface{}, object interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return IsType(t, expectedType, object, append([]interface{}{msg}, args...)...) +} + +// JSONEqf asserts that two JSON strings are equivalent. +// +// assert.JSONEqf(t, `{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`, "error message %s", "formatted") +func JSONEqf(t TestingT, expected string, actual string, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return JSONEq(t, expected, actual, append([]interface{}{msg}, args...)...) +} + +// YAMLEqf asserts that two YAML strings are equivalent. +func YAMLEqf(t TestingT, expected string, actual string, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return YAMLEq(t, expected, actual, append([]interface{}{msg}, args...)...) +} + +// Lenf asserts that the specified object has specific length. +// Lenf also fails if the object has a type that len() not accept. +// +// assert.Lenf(t, mySlice, 3, "error message %s", "formatted") +func Lenf(t TestingT, object interface{}, length int, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return Len(t, object, length, append([]interface{}{msg}, args...)...) +} + +// Lessf asserts that the first element is less than the second +// +// assert.Lessf(t, 1, 2, "error message %s", "formatted") +// assert.Lessf(t, float64(1, "error message %s", "formatted"), float64(2)) +// assert.Lessf(t, "a", "b", "error message %s", "formatted") +func Lessf(t TestingT, e1 interface{}, e2 interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return Less(t, e1, e2, append([]interface{}{msg}, args...)...) +} + +// LessOrEqualf asserts that the first element is less than or equal to the second +// +// assert.LessOrEqualf(t, 1, 2, "error message %s", "formatted") +// assert.LessOrEqualf(t, 2, 2, "error message %s", "formatted") +// assert.LessOrEqualf(t, "a", "b", "error message %s", "formatted") +// assert.LessOrEqualf(t, "b", "b", "error message %s", "formatted") +func LessOrEqualf(t TestingT, e1 interface{}, e2 interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return LessOrEqual(t, e1, e2, append([]interface{}{msg}, args...)...) +} + +// Nilf asserts that the specified object is nil. +// +// assert.Nilf(t, err, "error message %s", "formatted") +func Nilf(t TestingT, object interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return Nil(t, object, append([]interface{}{msg}, args...)...) +} + +// NoErrorf asserts that a function returned no error (i.e. `nil`). +// +// actualObj, err := SomeFunction() +// if assert.NoErrorf(t, err, "error message %s", "formatted") { +// assert.Equal(t, expectedObj, actualObj) +// } +func NoErrorf(t TestingT, err error, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return NoError(t, err, append([]interface{}{msg}, args...)...) +} + +// NotContainsf asserts that the specified string, list(array, slice...) or map does NOT contain the +// specified substring or element. +// +// assert.NotContainsf(t, "Hello World", "Earth", "error message %s", "formatted") +// assert.NotContainsf(t, ["Hello", "World"], "Earth", "error message %s", "formatted") +// assert.NotContainsf(t, {"Hello": "World"}, "Earth", "error message %s", "formatted") +func NotContainsf(t TestingT, s interface{}, contains interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return NotContains(t, s, contains, append([]interface{}{msg}, args...)...) +} + +// NotEmptyf asserts that the specified object is NOT empty. I.e. not nil, "", false, 0 or either +// a slice or a channel with len == 0. +// +// if assert.NotEmptyf(t, obj, "error message %s", "formatted") { +// assert.Equal(t, "two", obj[1]) +// } +func NotEmptyf(t TestingT, object interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return NotEmpty(t, object, append([]interface{}{msg}, args...)...) +} + +// NotEqualf asserts that the specified values are NOT equal. +// +// assert.NotEqualf(t, obj1, obj2, "error message %s", "formatted") +// +// Pointer variable equality is determined based on the equality of the +// referenced values (as opposed to the memory addresses). +func NotEqualf(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return NotEqual(t, expected, actual, append([]interface{}{msg}, args...)...) +} + +// NotNilf asserts that the specified object is not nil. +// +// assert.NotNilf(t, err, "error message %s", "formatted") +func NotNilf(t TestingT, object interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return NotNil(t, object, append([]interface{}{msg}, args...)...) +} + +// NotPanicsf asserts that the code inside the specified PanicTestFunc does NOT panic. +// +// assert.NotPanicsf(t, func(){ RemainCalm() }, "error message %s", "formatted") +func NotPanicsf(t TestingT, f PanicTestFunc, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return NotPanics(t, f, append([]interface{}{msg}, args...)...) +} + +// NotRegexpf asserts that a specified regexp does not match a string. +// +// assert.NotRegexpf(t, regexp.MustCompile("starts", "error message %s", "formatted"), "it's starting") +// assert.NotRegexpf(t, "^start", "it's not starting", "error message %s", "formatted") +func NotRegexpf(t TestingT, rx interface{}, str interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return NotRegexp(t, rx, str, append([]interface{}{msg}, args...)...) +} + +// NotSubsetf asserts that the specified list(array, slice...) contains not all +// elements given in the specified subset(array, slice...). +// +// assert.NotSubsetf(t, [1, 3, 4], [1, 2], "But [1, 3, 4] does not contain [1, 2]", "error message %s", "formatted") +func NotSubsetf(t TestingT, list interface{}, subset interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return NotSubset(t, list, subset, append([]interface{}{msg}, args...)...) +} + +// NotZerof asserts that i is not the zero value for its type. +func NotZerof(t TestingT, i interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return NotZero(t, i, append([]interface{}{msg}, args...)...) +} + +// Panicsf asserts that the code inside the specified PanicTestFunc panics. +// +// assert.Panicsf(t, func(){ GoCrazy() }, "error message %s", "formatted") +func Panicsf(t TestingT, f PanicTestFunc, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return Panics(t, f, append([]interface{}{msg}, args...)...) +} + +// PanicsWithValuef asserts that the code inside the specified PanicTestFunc panics, and that +// the recovered panic value equals the expected panic value. +// +// assert.PanicsWithValuef(t, "crazy error", func(){ GoCrazy() }, "error message %s", "formatted") +func PanicsWithValuef(t TestingT, expected interface{}, f PanicTestFunc, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return PanicsWithValue(t, expected, f, append([]interface{}{msg}, args...)...) +} + +// Regexpf asserts that a specified regexp matches a string. +// +// assert.Regexpf(t, regexp.MustCompile("start", "error message %s", "formatted"), "it's starting") +// assert.Regexpf(t, "start...$", "it's not starting", "error message %s", "formatted") +func Regexpf(t TestingT, rx interface{}, str interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return Regexp(t, rx, str, append([]interface{}{msg}, args...)...) +} + +// Samef asserts that two pointers reference the same object. +// +// assert.Samef(t, ptr1, ptr2, "error message %s", "formatted") +// +// Both arguments must be pointer variables. Pointer variable sameness is +// determined based on the equality of both type and value. +func Samef(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return Same(t, expected, actual, append([]interface{}{msg}, args...)...) +} + +// Subsetf asserts that the specified list(array, slice...) contains all +// elements given in the specified subset(array, slice...). +// +// assert.Subsetf(t, [1, 2, 3], [1, 2], "But [1, 2, 3] does contain [1, 2]", "error message %s", "formatted") +func Subsetf(t TestingT, list interface{}, subset interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return Subset(t, list, subset, append([]interface{}{msg}, args...)...) +} + +// Truef asserts that the specified value is true. +// +// assert.Truef(t, myBool, "error message %s", "formatted") +func Truef(t TestingT, value bool, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return True(t, value, append([]interface{}{msg}, args...)...) +} + +// WithinDurationf asserts that the two times are within duration delta of each other. +// +// assert.WithinDurationf(t, time.Now(), time.Now(), 10*time.Second, "error message %s", "formatted") +func WithinDurationf(t TestingT, expected time.Time, actual time.Time, delta time.Duration, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return WithinDuration(t, expected, actual, delta, append([]interface{}{msg}, args...)...) +} + +// Zerof asserts that i is the zero value for its type. +func Zerof(t TestingT, i interface{}, msg string, args ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + return Zero(t, i, append([]interface{}{msg}, args...)...) +} diff --git a/vendor/github.com/stretchr/testify/assert/assertion_forward.go b/vendor/github.com/stretchr/testify/assert/assertion_forward.go new file mode 100644 index 000000000..26830403a --- /dev/null +++ b/vendor/github.com/stretchr/testify/assert/assertion_forward.go @@ -0,0 +1,1120 @@ +/* +* CODE GENERATED AUTOMATICALLY WITH github.com/stretchr/testify/_codegen +* THIS FILE MUST NOT BE EDITED BY HAND + */ + +package assert + +import ( + http "net/http" + url "net/url" + time "time" +) + +// Condition uses a Comparison to assert a complex condition. +func (a *Assertions) Condition(comp Comparison, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Condition(a.t, comp, msgAndArgs...) +} + +// Conditionf uses a Comparison to assert a complex condition. +func (a *Assertions) Conditionf(comp Comparison, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Conditionf(a.t, comp, msg, args...) +} + +// Contains asserts that the specified string, list(array, slice...) or map contains the +// specified substring or element. +// +// a.Contains("Hello World", "World") +// a.Contains(["Hello", "World"], "World") +// a.Contains({"Hello": "World"}, "Hello") +func (a *Assertions) Contains(s interface{}, contains interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Contains(a.t, s, contains, msgAndArgs...) +} + +// Containsf asserts that the specified string, list(array, slice...) or map contains the +// specified substring or element. +// +// a.Containsf("Hello World", "World", "error message %s", "formatted") +// a.Containsf(["Hello", "World"], "World", "error message %s", "formatted") +// a.Containsf({"Hello": "World"}, "Hello", "error message %s", "formatted") +func (a *Assertions) Containsf(s interface{}, contains interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Containsf(a.t, s, contains, msg, args...) +} + +// DirExists checks whether a directory exists in the given path. It also fails if the path is a file rather a directory or there is an error checking whether it exists. +func (a *Assertions) DirExists(path string, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return DirExists(a.t, path, msgAndArgs...) +} + +// DirExistsf checks whether a directory exists in the given path. It also fails if the path is a file rather a directory or there is an error checking whether it exists. +func (a *Assertions) DirExistsf(path string, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return DirExistsf(a.t, path, msg, args...) +} + +// ElementsMatch asserts that the specified listA(array, slice...) is equal to specified +// listB(array, slice...) ignoring the order of the elements. If there are duplicate elements, +// the number of appearances of each of them in both lists should match. +// +// a.ElementsMatch([1, 3, 2, 3], [1, 3, 3, 2]) +func (a *Assertions) ElementsMatch(listA interface{}, listB interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return ElementsMatch(a.t, listA, listB, msgAndArgs...) +} + +// ElementsMatchf asserts that the specified listA(array, slice...) is equal to specified +// listB(array, slice...) ignoring the order of the elements. If there are duplicate elements, +// the number of appearances of each of them in both lists should match. +// +// a.ElementsMatchf([1, 3, 2, 3], [1, 3, 3, 2], "error message %s", "formatted") +func (a *Assertions) ElementsMatchf(listA interface{}, listB interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return ElementsMatchf(a.t, listA, listB, msg, args...) +} + +// Empty asserts that the specified object is empty. I.e. nil, "", false, 0 or either +// a slice or a channel with len == 0. +// +// a.Empty(obj) +func (a *Assertions) Empty(object interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Empty(a.t, object, msgAndArgs...) +} + +// Emptyf asserts that the specified object is empty. I.e. nil, "", false, 0 or either +// a slice or a channel with len == 0. +// +// a.Emptyf(obj, "error message %s", "formatted") +func (a *Assertions) Emptyf(object interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Emptyf(a.t, object, msg, args...) +} + +// Equal asserts that two objects are equal. +// +// a.Equal(123, 123) +// +// Pointer variable equality is determined based on the equality of the +// referenced values (as opposed to the memory addresses). Function equality +// cannot be determined and will always fail. +func (a *Assertions) Equal(expected interface{}, actual interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Equal(a.t, expected, actual, msgAndArgs...) +} + +// EqualError asserts that a function returned an error (i.e. not `nil`) +// and that it is equal to the provided error. +// +// actualObj, err := SomeFunction() +// a.EqualError(err, expectedErrorString) +func (a *Assertions) EqualError(theError error, errString string, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return EqualError(a.t, theError, errString, msgAndArgs...) +} + +// EqualErrorf asserts that a function returned an error (i.e. not `nil`) +// and that it is equal to the provided error. +// +// actualObj, err := SomeFunction() +// a.EqualErrorf(err, expectedErrorString, "error message %s", "formatted") +func (a *Assertions) EqualErrorf(theError error, errString string, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return EqualErrorf(a.t, theError, errString, msg, args...) +} + +// EqualValues asserts that two objects are equal or convertable to the same types +// and equal. +// +// a.EqualValues(uint32(123), int32(123)) +func (a *Assertions) EqualValues(expected interface{}, actual interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return EqualValues(a.t, expected, actual, msgAndArgs...) +} + +// EqualValuesf asserts that two objects are equal or convertable to the same types +// and equal. +// +// a.EqualValuesf(uint32(123, "error message %s", "formatted"), int32(123)) +func (a *Assertions) EqualValuesf(expected interface{}, actual interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return EqualValuesf(a.t, expected, actual, msg, args...) +} + +// Equalf asserts that two objects are equal. +// +// a.Equalf(123, 123, "error message %s", "formatted") +// +// Pointer variable equality is determined based on the equality of the +// referenced values (as opposed to the memory addresses). Function equality +// cannot be determined and will always fail. +func (a *Assertions) Equalf(expected interface{}, actual interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Equalf(a.t, expected, actual, msg, args...) +} + +// Error asserts that a function returned an error (i.e. not `nil`). +// +// actualObj, err := SomeFunction() +// if a.Error(err) { +// assert.Equal(t, expectedError, err) +// } +func (a *Assertions) Error(err error, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Error(a.t, err, msgAndArgs...) +} + +// Errorf asserts that a function returned an error (i.e. not `nil`). +// +// actualObj, err := SomeFunction() +// if a.Errorf(err, "error message %s", "formatted") { +// assert.Equal(t, expectedErrorf, err) +// } +func (a *Assertions) Errorf(err error, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Errorf(a.t, err, msg, args...) +} + +// Eventually asserts that given condition will be met in waitFor time, +// periodically checking target function each tick. +// +// a.Eventually(func() bool { return true; }, time.Second, 10*time.Millisecond) +func (a *Assertions) Eventually(condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Eventually(a.t, condition, waitFor, tick, msgAndArgs...) +} + +// Eventuallyf asserts that given condition will be met in waitFor time, +// periodically checking target function each tick. +// +// a.Eventuallyf(func() bool { return true; }, time.Second, 10*time.Millisecond, "error message %s", "formatted") +func (a *Assertions) Eventuallyf(condition func() bool, waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Eventuallyf(a.t, condition, waitFor, tick, msg, args...) +} + +// Exactly asserts that two objects are equal in value and type. +// +// a.Exactly(int32(123), int64(123)) +func (a *Assertions) Exactly(expected interface{}, actual interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Exactly(a.t, expected, actual, msgAndArgs...) +} + +// Exactlyf asserts that two objects are equal in value and type. +// +// a.Exactlyf(int32(123, "error message %s", "formatted"), int64(123)) +func (a *Assertions) Exactlyf(expected interface{}, actual interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Exactlyf(a.t, expected, actual, msg, args...) +} + +// Fail reports a failure through +func (a *Assertions) Fail(failureMessage string, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Fail(a.t, failureMessage, msgAndArgs...) +} + +// FailNow fails test +func (a *Assertions) FailNow(failureMessage string, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return FailNow(a.t, failureMessage, msgAndArgs...) +} + +// FailNowf fails test +func (a *Assertions) FailNowf(failureMessage string, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return FailNowf(a.t, failureMessage, msg, args...) +} + +// Failf reports a failure through +func (a *Assertions) Failf(failureMessage string, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Failf(a.t, failureMessage, msg, args...) +} + +// False asserts that the specified value is false. +// +// a.False(myBool) +func (a *Assertions) False(value bool, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return False(a.t, value, msgAndArgs...) +} + +// Falsef asserts that the specified value is false. +// +// a.Falsef(myBool, "error message %s", "formatted") +func (a *Assertions) Falsef(value bool, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Falsef(a.t, value, msg, args...) +} + +// FileExists checks whether a file exists in the given path. It also fails if the path points to a directory or there is an error when trying to check the file. +func (a *Assertions) FileExists(path string, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return FileExists(a.t, path, msgAndArgs...) +} + +// FileExistsf checks whether a file exists in the given path. It also fails if the path points to a directory or there is an error when trying to check the file. +func (a *Assertions) FileExistsf(path string, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return FileExistsf(a.t, path, msg, args...) +} + +// Greater asserts that the first element is greater than the second +// +// a.Greater(2, 1) +// a.Greater(float64(2), float64(1)) +// a.Greater("b", "a") +func (a *Assertions) Greater(e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Greater(a.t, e1, e2, msgAndArgs...) +} + +// GreaterOrEqual asserts that the first element is greater than or equal to the second +// +// a.GreaterOrEqual(2, 1) +// a.GreaterOrEqual(2, 2) +// a.GreaterOrEqual("b", "a") +// a.GreaterOrEqual("b", "b") +func (a *Assertions) GreaterOrEqual(e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return GreaterOrEqual(a.t, e1, e2, msgAndArgs...) +} + +// GreaterOrEqualf asserts that the first element is greater than or equal to the second +// +// a.GreaterOrEqualf(2, 1, "error message %s", "formatted") +// a.GreaterOrEqualf(2, 2, "error message %s", "formatted") +// a.GreaterOrEqualf("b", "a", "error message %s", "formatted") +// a.GreaterOrEqualf("b", "b", "error message %s", "formatted") +func (a *Assertions) GreaterOrEqualf(e1 interface{}, e2 interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return GreaterOrEqualf(a.t, e1, e2, msg, args...) +} + +// Greaterf asserts that the first element is greater than the second +// +// a.Greaterf(2, 1, "error message %s", "formatted") +// a.Greaterf(float64(2, "error message %s", "formatted"), float64(1)) +// a.Greaterf("b", "a", "error message %s", "formatted") +func (a *Assertions) Greaterf(e1 interface{}, e2 interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Greaterf(a.t, e1, e2, msg, args...) +} + +// HTTPBodyContains asserts that a specified handler returns a +// body that contains a string. +// +// a.HTTPBodyContains(myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPBodyContains(handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return HTTPBodyContains(a.t, handler, method, url, values, str, msgAndArgs...) +} + +// HTTPBodyContainsf asserts that a specified handler returns a +// body that contains a string. +// +// a.HTTPBodyContainsf(myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky", "error message %s", "formatted") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPBodyContainsf(handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return HTTPBodyContainsf(a.t, handler, method, url, values, str, msg, args...) +} + +// HTTPBodyNotContains asserts that a specified handler returns a +// body that does not contain a string. +// +// a.HTTPBodyNotContains(myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPBodyNotContains(handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return HTTPBodyNotContains(a.t, handler, method, url, values, str, msgAndArgs...) +} + +// HTTPBodyNotContainsf asserts that a specified handler returns a +// body that does not contain a string. +// +// a.HTTPBodyNotContainsf(myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky", "error message %s", "formatted") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPBodyNotContainsf(handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return HTTPBodyNotContainsf(a.t, handler, method, url, values, str, msg, args...) +} + +// HTTPError asserts that a specified handler returns an error status code. +// +// a.HTTPError(myHandler, "POST", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPError(handler http.HandlerFunc, method string, url string, values url.Values, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return HTTPError(a.t, handler, method, url, values, msgAndArgs...) +} + +// HTTPErrorf asserts that a specified handler returns an error status code. +// +// a.HTTPErrorf(myHandler, "POST", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// +// Returns whether the assertion was successful (true, "error message %s", "formatted") or not (false). +func (a *Assertions) HTTPErrorf(handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return HTTPErrorf(a.t, handler, method, url, values, msg, args...) +} + +// HTTPRedirect asserts that a specified handler returns a redirect status code. +// +// a.HTTPRedirect(myHandler, "GET", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPRedirect(handler http.HandlerFunc, method string, url string, values url.Values, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return HTTPRedirect(a.t, handler, method, url, values, msgAndArgs...) +} + +// HTTPRedirectf asserts that a specified handler returns a redirect status code. +// +// a.HTTPRedirectf(myHandler, "GET", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// +// Returns whether the assertion was successful (true, "error message %s", "formatted") or not (false). +func (a *Assertions) HTTPRedirectf(handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return HTTPRedirectf(a.t, handler, method, url, values, msg, args...) +} + +// HTTPSuccess asserts that a specified handler returns a success status code. +// +// a.HTTPSuccess(myHandler, "POST", "http://www.google.com", nil) +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPSuccess(handler http.HandlerFunc, method string, url string, values url.Values, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return HTTPSuccess(a.t, handler, method, url, values, msgAndArgs...) +} + +// HTTPSuccessf asserts that a specified handler returns a success status code. +// +// a.HTTPSuccessf(myHandler, "POST", "http://www.google.com", nil, "error message %s", "formatted") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPSuccessf(handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return HTTPSuccessf(a.t, handler, method, url, values, msg, args...) +} + +// Implements asserts that an object is implemented by the specified interface. +// +// a.Implements((*MyInterface)(nil), new(MyObject)) +func (a *Assertions) Implements(interfaceObject interface{}, object interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Implements(a.t, interfaceObject, object, msgAndArgs...) +} + +// Implementsf asserts that an object is implemented by the specified interface. +// +// a.Implementsf((*MyInterface, "error message %s", "formatted")(nil), new(MyObject)) +func (a *Assertions) Implementsf(interfaceObject interface{}, object interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Implementsf(a.t, interfaceObject, object, msg, args...) +} + +// InDelta asserts that the two numerals are within delta of each other. +// +// a.InDelta(math.Pi, (22 / 7.0), 0.01) +func (a *Assertions) InDelta(expected interface{}, actual interface{}, delta float64, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return InDelta(a.t, expected, actual, delta, msgAndArgs...) +} + +// InDeltaMapValues is the same as InDelta, but it compares all values between two maps. Both maps must have exactly the same keys. +func (a *Assertions) InDeltaMapValues(expected interface{}, actual interface{}, delta float64, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return InDeltaMapValues(a.t, expected, actual, delta, msgAndArgs...) +} + +// InDeltaMapValuesf is the same as InDelta, but it compares all values between two maps. Both maps must have exactly the same keys. +func (a *Assertions) InDeltaMapValuesf(expected interface{}, actual interface{}, delta float64, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return InDeltaMapValuesf(a.t, expected, actual, delta, msg, args...) +} + +// InDeltaSlice is the same as InDelta, except it compares two slices. +func (a *Assertions) InDeltaSlice(expected interface{}, actual interface{}, delta float64, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return InDeltaSlice(a.t, expected, actual, delta, msgAndArgs...) +} + +// InDeltaSlicef is the same as InDelta, except it compares two slices. +func (a *Assertions) InDeltaSlicef(expected interface{}, actual interface{}, delta float64, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return InDeltaSlicef(a.t, expected, actual, delta, msg, args...) +} + +// InDeltaf asserts that the two numerals are within delta of each other. +// +// a.InDeltaf(math.Pi, (22 / 7.0, "error message %s", "formatted"), 0.01) +func (a *Assertions) InDeltaf(expected interface{}, actual interface{}, delta float64, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return InDeltaf(a.t, expected, actual, delta, msg, args...) +} + +// InEpsilon asserts that expected and actual have a relative error less than epsilon +func (a *Assertions) InEpsilon(expected interface{}, actual interface{}, epsilon float64, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return InEpsilon(a.t, expected, actual, epsilon, msgAndArgs...) +} + +// InEpsilonSlice is the same as InEpsilon, except it compares each value from two slices. +func (a *Assertions) InEpsilonSlice(expected interface{}, actual interface{}, epsilon float64, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return InEpsilonSlice(a.t, expected, actual, epsilon, msgAndArgs...) +} + +// InEpsilonSlicef is the same as InEpsilon, except it compares each value from two slices. +func (a *Assertions) InEpsilonSlicef(expected interface{}, actual interface{}, epsilon float64, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return InEpsilonSlicef(a.t, expected, actual, epsilon, msg, args...) +} + +// InEpsilonf asserts that expected and actual have a relative error less than epsilon +func (a *Assertions) InEpsilonf(expected interface{}, actual interface{}, epsilon float64, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return InEpsilonf(a.t, expected, actual, epsilon, msg, args...) +} + +// IsType asserts that the specified objects are of the same type. +func (a *Assertions) IsType(expectedType interface{}, object interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return IsType(a.t, expectedType, object, msgAndArgs...) +} + +// IsTypef asserts that the specified objects are of the same type. +func (a *Assertions) IsTypef(expectedType interface{}, object interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return IsTypef(a.t, expectedType, object, msg, args...) +} + +// JSONEq asserts that two JSON strings are equivalent. +// +// a.JSONEq(`{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`) +func (a *Assertions) JSONEq(expected string, actual string, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return JSONEq(a.t, expected, actual, msgAndArgs...) +} + +// JSONEqf asserts that two JSON strings are equivalent. +// +// a.JSONEqf(`{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`, "error message %s", "formatted") +func (a *Assertions) JSONEqf(expected string, actual string, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return JSONEqf(a.t, expected, actual, msg, args...) +} + +// YAMLEq asserts that two YAML strings are equivalent. +func (a *Assertions) YAMLEq(expected string, actual string, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return YAMLEq(a.t, expected, actual, msgAndArgs...) +} + +// YAMLEqf asserts that two YAML strings are equivalent. +func (a *Assertions) YAMLEqf(expected string, actual string, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return YAMLEqf(a.t, expected, actual, msg, args...) +} + +// Len asserts that the specified object has specific length. +// Len also fails if the object has a type that len() not accept. +// +// a.Len(mySlice, 3) +func (a *Assertions) Len(object interface{}, length int, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Len(a.t, object, length, msgAndArgs...) +} + +// Lenf asserts that the specified object has specific length. +// Lenf also fails if the object has a type that len() not accept. +// +// a.Lenf(mySlice, 3, "error message %s", "formatted") +func (a *Assertions) Lenf(object interface{}, length int, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Lenf(a.t, object, length, msg, args...) +} + +// Less asserts that the first element is less than the second +// +// a.Less(1, 2) +// a.Less(float64(1), float64(2)) +// a.Less("a", "b") +func (a *Assertions) Less(e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Less(a.t, e1, e2, msgAndArgs...) +} + +// LessOrEqual asserts that the first element is less than or equal to the second +// +// a.LessOrEqual(1, 2) +// a.LessOrEqual(2, 2) +// a.LessOrEqual("a", "b") +// a.LessOrEqual("b", "b") +func (a *Assertions) LessOrEqual(e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return LessOrEqual(a.t, e1, e2, msgAndArgs...) +} + +// LessOrEqualf asserts that the first element is less than or equal to the second +// +// a.LessOrEqualf(1, 2, "error message %s", "formatted") +// a.LessOrEqualf(2, 2, "error message %s", "formatted") +// a.LessOrEqualf("a", "b", "error message %s", "formatted") +// a.LessOrEqualf("b", "b", "error message %s", "formatted") +func (a *Assertions) LessOrEqualf(e1 interface{}, e2 interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return LessOrEqualf(a.t, e1, e2, msg, args...) +} + +// Lessf asserts that the first element is less than the second +// +// a.Lessf(1, 2, "error message %s", "formatted") +// a.Lessf(float64(1, "error message %s", "formatted"), float64(2)) +// a.Lessf("a", "b", "error message %s", "formatted") +func (a *Assertions) Lessf(e1 interface{}, e2 interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Lessf(a.t, e1, e2, msg, args...) +} + +// Nil asserts that the specified object is nil. +// +// a.Nil(err) +func (a *Assertions) Nil(object interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Nil(a.t, object, msgAndArgs...) +} + +// Nilf asserts that the specified object is nil. +// +// a.Nilf(err, "error message %s", "formatted") +func (a *Assertions) Nilf(object interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Nilf(a.t, object, msg, args...) +} + +// NoError asserts that a function returned no error (i.e. `nil`). +// +// actualObj, err := SomeFunction() +// if a.NoError(err) { +// assert.Equal(t, expectedObj, actualObj) +// } +func (a *Assertions) NoError(err error, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NoError(a.t, err, msgAndArgs...) +} + +// NoErrorf asserts that a function returned no error (i.e. `nil`). +// +// actualObj, err := SomeFunction() +// if a.NoErrorf(err, "error message %s", "formatted") { +// assert.Equal(t, expectedObj, actualObj) +// } +func (a *Assertions) NoErrorf(err error, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NoErrorf(a.t, err, msg, args...) +} + +// NotContains asserts that the specified string, list(array, slice...) or map does NOT contain the +// specified substring or element. +// +// a.NotContains("Hello World", "Earth") +// a.NotContains(["Hello", "World"], "Earth") +// a.NotContains({"Hello": "World"}, "Earth") +func (a *Assertions) NotContains(s interface{}, contains interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NotContains(a.t, s, contains, msgAndArgs...) +} + +// NotContainsf asserts that the specified string, list(array, slice...) or map does NOT contain the +// specified substring or element. +// +// a.NotContainsf("Hello World", "Earth", "error message %s", "formatted") +// a.NotContainsf(["Hello", "World"], "Earth", "error message %s", "formatted") +// a.NotContainsf({"Hello": "World"}, "Earth", "error message %s", "formatted") +func (a *Assertions) NotContainsf(s interface{}, contains interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NotContainsf(a.t, s, contains, msg, args...) +} + +// NotEmpty asserts that the specified object is NOT empty. I.e. not nil, "", false, 0 or either +// a slice or a channel with len == 0. +// +// if a.NotEmpty(obj) { +// assert.Equal(t, "two", obj[1]) +// } +func (a *Assertions) NotEmpty(object interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NotEmpty(a.t, object, msgAndArgs...) +} + +// NotEmptyf asserts that the specified object is NOT empty. I.e. not nil, "", false, 0 or either +// a slice or a channel with len == 0. +// +// if a.NotEmptyf(obj, "error message %s", "formatted") { +// assert.Equal(t, "two", obj[1]) +// } +func (a *Assertions) NotEmptyf(object interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NotEmptyf(a.t, object, msg, args...) +} + +// NotEqual asserts that the specified values are NOT equal. +// +// a.NotEqual(obj1, obj2) +// +// Pointer variable equality is determined based on the equality of the +// referenced values (as opposed to the memory addresses). +func (a *Assertions) NotEqual(expected interface{}, actual interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NotEqual(a.t, expected, actual, msgAndArgs...) +} + +// NotEqualf asserts that the specified values are NOT equal. +// +// a.NotEqualf(obj1, obj2, "error message %s", "formatted") +// +// Pointer variable equality is determined based on the equality of the +// referenced values (as opposed to the memory addresses). +func (a *Assertions) NotEqualf(expected interface{}, actual interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NotEqualf(a.t, expected, actual, msg, args...) +} + +// NotNil asserts that the specified object is not nil. +// +// a.NotNil(err) +func (a *Assertions) NotNil(object interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NotNil(a.t, object, msgAndArgs...) +} + +// NotNilf asserts that the specified object is not nil. +// +// a.NotNilf(err, "error message %s", "formatted") +func (a *Assertions) NotNilf(object interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NotNilf(a.t, object, msg, args...) +} + +// NotPanics asserts that the code inside the specified PanicTestFunc does NOT panic. +// +// a.NotPanics(func(){ RemainCalm() }) +func (a *Assertions) NotPanics(f PanicTestFunc, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NotPanics(a.t, f, msgAndArgs...) +} + +// NotPanicsf asserts that the code inside the specified PanicTestFunc does NOT panic. +// +// a.NotPanicsf(func(){ RemainCalm() }, "error message %s", "formatted") +func (a *Assertions) NotPanicsf(f PanicTestFunc, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NotPanicsf(a.t, f, msg, args...) +} + +// NotRegexp asserts that a specified regexp does not match a string. +// +// a.NotRegexp(regexp.MustCompile("starts"), "it's starting") +// a.NotRegexp("^start", "it's not starting") +func (a *Assertions) NotRegexp(rx interface{}, str interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NotRegexp(a.t, rx, str, msgAndArgs...) +} + +// NotRegexpf asserts that a specified regexp does not match a string. +// +// a.NotRegexpf(regexp.MustCompile("starts", "error message %s", "formatted"), "it's starting") +// a.NotRegexpf("^start", "it's not starting", "error message %s", "formatted") +func (a *Assertions) NotRegexpf(rx interface{}, str interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NotRegexpf(a.t, rx, str, msg, args...) +} + +// NotSubset asserts that the specified list(array, slice...) contains not all +// elements given in the specified subset(array, slice...). +// +// a.NotSubset([1, 3, 4], [1, 2], "But [1, 3, 4] does not contain [1, 2]") +func (a *Assertions) NotSubset(list interface{}, subset interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NotSubset(a.t, list, subset, msgAndArgs...) +} + +// NotSubsetf asserts that the specified list(array, slice...) contains not all +// elements given in the specified subset(array, slice...). +// +// a.NotSubsetf([1, 3, 4], [1, 2], "But [1, 3, 4] does not contain [1, 2]", "error message %s", "formatted") +func (a *Assertions) NotSubsetf(list interface{}, subset interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NotSubsetf(a.t, list, subset, msg, args...) +} + +// NotZero asserts that i is not the zero value for its type. +func (a *Assertions) NotZero(i interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NotZero(a.t, i, msgAndArgs...) +} + +// NotZerof asserts that i is not the zero value for its type. +func (a *Assertions) NotZerof(i interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return NotZerof(a.t, i, msg, args...) +} + +// Panics asserts that the code inside the specified PanicTestFunc panics. +// +// a.Panics(func(){ GoCrazy() }) +func (a *Assertions) Panics(f PanicTestFunc, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Panics(a.t, f, msgAndArgs...) +} + +// PanicsWithValue asserts that the code inside the specified PanicTestFunc panics, and that +// the recovered panic value equals the expected panic value. +// +// a.PanicsWithValue("crazy error", func(){ GoCrazy() }) +func (a *Assertions) PanicsWithValue(expected interface{}, f PanicTestFunc, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return PanicsWithValue(a.t, expected, f, msgAndArgs...) +} + +// PanicsWithValuef asserts that the code inside the specified PanicTestFunc panics, and that +// the recovered panic value equals the expected panic value. +// +// a.PanicsWithValuef("crazy error", func(){ GoCrazy() }, "error message %s", "formatted") +func (a *Assertions) PanicsWithValuef(expected interface{}, f PanicTestFunc, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return PanicsWithValuef(a.t, expected, f, msg, args...) +} + +// Panicsf asserts that the code inside the specified PanicTestFunc panics. +// +// a.Panicsf(func(){ GoCrazy() }, "error message %s", "formatted") +func (a *Assertions) Panicsf(f PanicTestFunc, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Panicsf(a.t, f, msg, args...) +} + +// Regexp asserts that a specified regexp matches a string. +// +// a.Regexp(regexp.MustCompile("start"), "it's starting") +// a.Regexp("start...$", "it's not starting") +func (a *Assertions) Regexp(rx interface{}, str interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Regexp(a.t, rx, str, msgAndArgs...) +} + +// Regexpf asserts that a specified regexp matches a string. +// +// a.Regexpf(regexp.MustCompile("start", "error message %s", "formatted"), "it's starting") +// a.Regexpf("start...$", "it's not starting", "error message %s", "formatted") +func (a *Assertions) Regexpf(rx interface{}, str interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Regexpf(a.t, rx, str, msg, args...) +} + +// Same asserts that two pointers reference the same object. +// +// a.Same(ptr1, ptr2) +// +// Both arguments must be pointer variables. Pointer variable sameness is +// determined based on the equality of both type and value. +func (a *Assertions) Same(expected interface{}, actual interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Same(a.t, expected, actual, msgAndArgs...) +} + +// Samef asserts that two pointers reference the same object. +// +// a.Samef(ptr1, ptr2, "error message %s", "formatted") +// +// Both arguments must be pointer variables. Pointer variable sameness is +// determined based on the equality of both type and value. +func (a *Assertions) Samef(expected interface{}, actual interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Samef(a.t, expected, actual, msg, args...) +} + +// Subset asserts that the specified list(array, slice...) contains all +// elements given in the specified subset(array, slice...). +// +// a.Subset([1, 2, 3], [1, 2], "But [1, 2, 3] does contain [1, 2]") +func (a *Assertions) Subset(list interface{}, subset interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Subset(a.t, list, subset, msgAndArgs...) +} + +// Subsetf asserts that the specified list(array, slice...) contains all +// elements given in the specified subset(array, slice...). +// +// a.Subsetf([1, 2, 3], [1, 2], "But [1, 2, 3] does contain [1, 2]", "error message %s", "formatted") +func (a *Assertions) Subsetf(list interface{}, subset interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Subsetf(a.t, list, subset, msg, args...) +} + +// True asserts that the specified value is true. +// +// a.True(myBool) +func (a *Assertions) True(value bool, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return True(a.t, value, msgAndArgs...) +} + +// Truef asserts that the specified value is true. +// +// a.Truef(myBool, "error message %s", "formatted") +func (a *Assertions) Truef(value bool, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Truef(a.t, value, msg, args...) +} + +// WithinDuration asserts that the two times are within duration delta of each other. +// +// a.WithinDuration(time.Now(), time.Now(), 10*time.Second) +func (a *Assertions) WithinDuration(expected time.Time, actual time.Time, delta time.Duration, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return WithinDuration(a.t, expected, actual, delta, msgAndArgs...) +} + +// WithinDurationf asserts that the two times are within duration delta of each other. +// +// a.WithinDurationf(time.Now(), time.Now(), 10*time.Second, "error message %s", "formatted") +func (a *Assertions) WithinDurationf(expected time.Time, actual time.Time, delta time.Duration, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return WithinDurationf(a.t, expected, actual, delta, msg, args...) +} + +// Zero asserts that i is the zero value for its type. +func (a *Assertions) Zero(i interface{}, msgAndArgs ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Zero(a.t, i, msgAndArgs...) +} + +// Zerof asserts that i is the zero value for its type. +func (a *Assertions) Zerof(i interface{}, msg string, args ...interface{}) bool { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + return Zerof(a.t, i, msg, args...) +} diff --git a/vendor/github.com/stretchr/testify/assert/assertion_order.go b/vendor/github.com/stretchr/testify/assert/assertion_order.go new file mode 100644 index 000000000..15a486ca6 --- /dev/null +++ b/vendor/github.com/stretchr/testify/assert/assertion_order.go @@ -0,0 +1,309 @@ +package assert + +import ( + "fmt" + "reflect" +) + +func compare(obj1, obj2 interface{}, kind reflect.Kind) (int, bool) { + switch kind { + case reflect.Int: + { + intobj1 := obj1.(int) + intobj2 := obj2.(int) + if intobj1 > intobj2 { + return -1, true + } + if intobj1 == intobj2 { + return 0, true + } + if intobj1 < intobj2 { + return 1, true + } + } + case reflect.Int8: + { + int8obj1 := obj1.(int8) + int8obj2 := obj2.(int8) + if int8obj1 > int8obj2 { + return -1, true + } + if int8obj1 == int8obj2 { + return 0, true + } + if int8obj1 < int8obj2 { + return 1, true + } + } + case reflect.Int16: + { + int16obj1 := obj1.(int16) + int16obj2 := obj2.(int16) + if int16obj1 > int16obj2 { + return -1, true + } + if int16obj1 == int16obj2 { + return 0, true + } + if int16obj1 < int16obj2 { + return 1, true + } + } + case reflect.Int32: + { + int32obj1 := obj1.(int32) + int32obj2 := obj2.(int32) + if int32obj1 > int32obj2 { + return -1, true + } + if int32obj1 == int32obj2 { + return 0, true + } + if int32obj1 < int32obj2 { + return 1, true + } + } + case reflect.Int64: + { + int64obj1 := obj1.(int64) + int64obj2 := obj2.(int64) + if int64obj1 > int64obj2 { + return -1, true + } + if int64obj1 == int64obj2 { + return 0, true + } + if int64obj1 < int64obj2 { + return 1, true + } + } + case reflect.Uint: + { + uintobj1 := obj1.(uint) + uintobj2 := obj2.(uint) + if uintobj1 > uintobj2 { + return -1, true + } + if uintobj1 == uintobj2 { + return 0, true + } + if uintobj1 < uintobj2 { + return 1, true + } + } + case reflect.Uint8: + { + uint8obj1 := obj1.(uint8) + uint8obj2 := obj2.(uint8) + if uint8obj1 > uint8obj2 { + return -1, true + } + if uint8obj1 == uint8obj2 { + return 0, true + } + if uint8obj1 < uint8obj2 { + return 1, true + } + } + case reflect.Uint16: + { + uint16obj1 := obj1.(uint16) + uint16obj2 := obj2.(uint16) + if uint16obj1 > uint16obj2 { + return -1, true + } + if uint16obj1 == uint16obj2 { + return 0, true + } + if uint16obj1 < uint16obj2 { + return 1, true + } + } + case reflect.Uint32: + { + uint32obj1 := obj1.(uint32) + uint32obj2 := obj2.(uint32) + if uint32obj1 > uint32obj2 { + return -1, true + } + if uint32obj1 == uint32obj2 { + return 0, true + } + if uint32obj1 < uint32obj2 { + return 1, true + } + } + case reflect.Uint64: + { + uint64obj1 := obj1.(uint64) + uint64obj2 := obj2.(uint64) + if uint64obj1 > uint64obj2 { + return -1, true + } + if uint64obj1 == uint64obj2 { + return 0, true + } + if uint64obj1 < uint64obj2 { + return 1, true + } + } + case reflect.Float32: + { + float32obj1 := obj1.(float32) + float32obj2 := obj2.(float32) + if float32obj1 > float32obj2 { + return -1, true + } + if float32obj1 == float32obj2 { + return 0, true + } + if float32obj1 < float32obj2 { + return 1, true + } + } + case reflect.Float64: + { + float64obj1 := obj1.(float64) + float64obj2 := obj2.(float64) + if float64obj1 > float64obj2 { + return -1, true + } + if float64obj1 == float64obj2 { + return 0, true + } + if float64obj1 < float64obj2 { + return 1, true + } + } + case reflect.String: + { + stringobj1 := obj1.(string) + stringobj2 := obj2.(string) + if stringobj1 > stringobj2 { + return -1, true + } + if stringobj1 == stringobj2 { + return 0, true + } + if stringobj1 < stringobj2 { + return 1, true + } + } + } + + return 0, false +} + +// Greater asserts that the first element is greater than the second +// +// assert.Greater(t, 2, 1) +// assert.Greater(t, float64(2), float64(1)) +// assert.Greater(t, "b", "a") +func Greater(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + + e1Kind := reflect.ValueOf(e1).Kind() + e2Kind := reflect.ValueOf(e2).Kind() + if e1Kind != e2Kind { + return Fail(t, "Elements should be the same type", msgAndArgs...) + } + + res, isComparable := compare(e1, e2, e1Kind) + if !isComparable { + return Fail(t, fmt.Sprintf("Can not compare type \"%s\"", reflect.TypeOf(e1)), msgAndArgs...) + } + + if res != -1 { + return Fail(t, fmt.Sprintf("\"%v\" is not greater than \"%v\"", e1, e2), msgAndArgs...) + } + + return true +} + +// GreaterOrEqual asserts that the first element is greater than or equal to the second +// +// assert.GreaterOrEqual(t, 2, 1) +// assert.GreaterOrEqual(t, 2, 2) +// assert.GreaterOrEqual(t, "b", "a") +// assert.GreaterOrEqual(t, "b", "b") +func GreaterOrEqual(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + + e1Kind := reflect.ValueOf(e1).Kind() + e2Kind := reflect.ValueOf(e2).Kind() + if e1Kind != e2Kind { + return Fail(t, "Elements should be the same type", msgAndArgs...) + } + + res, isComparable := compare(e1, e2, e1Kind) + if !isComparable { + return Fail(t, fmt.Sprintf("Can not compare type \"%s\"", reflect.TypeOf(e1)), msgAndArgs...) + } + + if res != -1 && res != 0 { + return Fail(t, fmt.Sprintf("\"%v\" is not greater than or equal to \"%v\"", e1, e2), msgAndArgs...) + } + + return true +} + +// Less asserts that the first element is less than the second +// +// assert.Less(t, 1, 2) +// assert.Less(t, float64(1), float64(2)) +// assert.Less(t, "a", "b") +func Less(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + + e1Kind := reflect.ValueOf(e1).Kind() + e2Kind := reflect.ValueOf(e2).Kind() + if e1Kind != e2Kind { + return Fail(t, "Elements should be the same type", msgAndArgs...) + } + + res, isComparable := compare(e1, e2, e1Kind) + if !isComparable { + return Fail(t, fmt.Sprintf("Can not compare type \"%s\"", reflect.TypeOf(e1)), msgAndArgs...) + } + + if res != 1 { + return Fail(t, fmt.Sprintf("\"%v\" is not less than \"%v\"", e1, e2), msgAndArgs...) + } + + return true +} + +// LessOrEqual asserts that the first element is less than or equal to the second +// +// assert.LessOrEqual(t, 1, 2) +// assert.LessOrEqual(t, 2, 2) +// assert.LessOrEqual(t, "a", "b") +// assert.LessOrEqual(t, "b", "b") +func LessOrEqual(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + + e1Kind := reflect.ValueOf(e1).Kind() + e2Kind := reflect.ValueOf(e2).Kind() + if e1Kind != e2Kind { + return Fail(t, "Elements should be the same type", msgAndArgs...) + } + + res, isComparable := compare(e1, e2, e1Kind) + if !isComparable { + return Fail(t, fmt.Sprintf("Can not compare type \"%s\"", reflect.TypeOf(e1)), msgAndArgs...) + } + + if res != 1 && res != 0 { + return Fail(t, fmt.Sprintf("\"%v\" is not less than or equal to \"%v\"", e1, e2), msgAndArgs...) + } + + return true +} diff --git a/vendor/github.com/stretchr/testify/assert/assertions.go b/vendor/github.com/stretchr/testify/assert/assertions.go new file mode 100644 index 000000000..044da8b01 --- /dev/null +++ b/vendor/github.com/stretchr/testify/assert/assertions.go @@ -0,0 +1,1498 @@ +package assert + +import ( + "bufio" + "bytes" + "encoding/json" + "errors" + "fmt" + "math" + "os" + "reflect" + "regexp" + "runtime" + "strings" + "time" + "unicode" + "unicode/utf8" + + "github.com/davecgh/go-spew/spew" + "github.com/pmezard/go-difflib/difflib" + yaml "gopkg.in/yaml.v2" +) + +//go:generate go run ../_codegen/main.go -output-package=assert -template=assertion_format.go.tmpl + +// TestingT is an interface wrapper around *testing.T +type TestingT interface { + Errorf(format string, args ...interface{}) +} + +// ComparisonAssertionFunc is a common function prototype when comparing two values. Can be useful +// for table driven tests. +type ComparisonAssertionFunc func(TestingT, interface{}, interface{}, ...interface{}) bool + +// ValueAssertionFunc is a common function prototype when validating a single value. Can be useful +// for table driven tests. +type ValueAssertionFunc func(TestingT, interface{}, ...interface{}) bool + +// BoolAssertionFunc is a common function prototype when validating a bool value. Can be useful +// for table driven tests. +type BoolAssertionFunc func(TestingT, bool, ...interface{}) bool + +// ErrorAssertionFunc is a common function prototype when validating an error value. Can be useful +// for table driven tests. +type ErrorAssertionFunc func(TestingT, error, ...interface{}) bool + +// Comparison a custom function that returns true on success and false on failure +type Comparison func() (success bool) + +/* + Helper functions +*/ + +// ObjectsAreEqual determines if two objects are considered equal. +// +// This function does no assertion of any kind. +func ObjectsAreEqual(expected, actual interface{}) bool { + if expected == nil || actual == nil { + return expected == actual + } + + exp, ok := expected.([]byte) + if !ok { + return reflect.DeepEqual(expected, actual) + } + + act, ok := actual.([]byte) + if !ok { + return false + } + if exp == nil || act == nil { + return exp == nil && act == nil + } + return bytes.Equal(exp, act) +} + +// ObjectsAreEqualValues gets whether two objects are equal, or if their +// values are equal. +func ObjectsAreEqualValues(expected, actual interface{}) bool { + if ObjectsAreEqual(expected, actual) { + return true + } + + actualType := reflect.TypeOf(actual) + if actualType == nil { + return false + } + expectedValue := reflect.ValueOf(expected) + if expectedValue.IsValid() && expectedValue.Type().ConvertibleTo(actualType) { + // Attempt comparison after type conversion + return reflect.DeepEqual(expectedValue.Convert(actualType).Interface(), actual) + } + + return false +} + +/* CallerInfo is necessary because the assert functions use the testing object +internally, causing it to print the file:line of the assert method, rather than where +the problem actually occurred in calling code.*/ + +// CallerInfo returns an array of strings containing the file and line number +// of each stack frame leading from the current test to the assert call that +// failed. +func CallerInfo() []string { + + pc := uintptr(0) + file := "" + line := 0 + ok := false + name := "" + + callers := []string{} + for i := 0; ; i++ { + pc, file, line, ok = runtime.Caller(i) + if !ok { + // The breaks below failed to terminate the loop, and we ran off the + // end of the call stack. + break + } + + // This is a huge edge case, but it will panic if this is the case, see #180 + if file == "" { + break + } + + f := runtime.FuncForPC(pc) + if f == nil { + break + } + name = f.Name() + + // testing.tRunner is the standard library function that calls + // tests. Subtests are called directly by tRunner, without going through + // the Test/Benchmark/Example function that contains the t.Run calls, so + // with subtests we should break when we hit tRunner, without adding it + // to the list of callers. + if name == "testing.tRunner" { + break + } + + parts := strings.Split(file, "/") + file = parts[len(parts)-1] + if len(parts) > 1 { + dir := parts[len(parts)-2] + if (dir != "assert" && dir != "mock" && dir != "require") || file == "mock_test.go" { + callers = append(callers, fmt.Sprintf("%s:%d", file, line)) + } + } + + // Drop the package + segments := strings.Split(name, ".") + name = segments[len(segments)-1] + if isTest(name, "Test") || + isTest(name, "Benchmark") || + isTest(name, "Example") { + break + } + } + + return callers +} + +// Stolen from the `go test` tool. +// isTest tells whether name looks like a test (or benchmark, according to prefix). +// It is a Test (say) if there is a character after Test that is not a lower-case letter. +// We don't want TesticularCancer. +func isTest(name, prefix string) bool { + if !strings.HasPrefix(name, prefix) { + return false + } + if len(name) == len(prefix) { // "Test" is ok + return true + } + rune, _ := utf8.DecodeRuneInString(name[len(prefix):]) + return !unicode.IsLower(rune) +} + +func messageFromMsgAndArgs(msgAndArgs ...interface{}) string { + if len(msgAndArgs) == 0 || msgAndArgs == nil { + return "" + } + if len(msgAndArgs) == 1 { + msg := msgAndArgs[0] + if msgAsStr, ok := msg.(string); ok { + return msgAsStr + } + return fmt.Sprintf("%+v", msg) + } + if len(msgAndArgs) > 1 { + return fmt.Sprintf(msgAndArgs[0].(string), msgAndArgs[1:]...) + } + return "" +} + +// Aligns the provided message so that all lines after the first line start at the same location as the first line. +// Assumes that the first line starts at the correct location (after carriage return, tab, label, spacer and tab). +// The longestLabelLen parameter specifies the length of the longest label in the output (required becaues this is the +// basis on which the alignment occurs). +func indentMessageLines(message string, longestLabelLen int) string { + outBuf := new(bytes.Buffer) + + for i, scanner := 0, bufio.NewScanner(strings.NewReader(message)); scanner.Scan(); i++ { + // no need to align first line because it starts at the correct location (after the label) + if i != 0 { + // append alignLen+1 spaces to align with "{{longestLabel}}:" before adding tab + outBuf.WriteString("\n\t" + strings.Repeat(" ", longestLabelLen+1) + "\t") + } + outBuf.WriteString(scanner.Text()) + } + + return outBuf.String() +} + +type failNower interface { + FailNow() +} + +// FailNow fails test +func FailNow(t TestingT, failureMessage string, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + Fail(t, failureMessage, msgAndArgs...) + + // We cannot extend TestingT with FailNow() and + // maintain backwards compatibility, so we fallback + // to panicking when FailNow is not available in + // TestingT. + // See issue #263 + + if t, ok := t.(failNower); ok { + t.FailNow() + } else { + panic("test failed and t is missing `FailNow()`") + } + return false +} + +// Fail reports a failure through +func Fail(t TestingT, failureMessage string, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + content := []labeledContent{ + {"Error Trace", strings.Join(CallerInfo(), "\n\t\t\t")}, + {"Error", failureMessage}, + } + + // Add test name if the Go version supports it + if n, ok := t.(interface { + Name() string + }); ok { + content = append(content, labeledContent{"Test", n.Name()}) + } + + message := messageFromMsgAndArgs(msgAndArgs...) + if len(message) > 0 { + content = append(content, labeledContent{"Messages", message}) + } + + t.Errorf("\n%s", ""+labeledOutput(content...)) + + return false +} + +type labeledContent struct { + label string + content string +} + +// labeledOutput returns a string consisting of the provided labeledContent. Each labeled output is appended in the following manner: +// +// \t{{label}}:{{align_spaces}}\t{{content}}\n +// +// The initial carriage return is required to undo/erase any padding added by testing.T.Errorf. The "\t{{label}}:" is for the label. +// If a label is shorter than the longest label provided, padding spaces are added to make all the labels match in length. Once this +// alignment is achieved, "\t{{content}}\n" is added for the output. +// +// If the content of the labeledOutput contains line breaks, the subsequent lines are aligned so that they start at the same location as the first line. +func labeledOutput(content ...labeledContent) string { + longestLabel := 0 + for _, v := range content { + if len(v.label) > longestLabel { + longestLabel = len(v.label) + } + } + var output string + for _, v := range content { + output += "\t" + v.label + ":" + strings.Repeat(" ", longestLabel-len(v.label)) + "\t" + indentMessageLines(v.content, longestLabel) + "\n" + } + return output +} + +// Implements asserts that an object is implemented by the specified interface. +// +// assert.Implements(t, (*MyInterface)(nil), new(MyObject)) +func Implements(t TestingT, interfaceObject interface{}, object interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + interfaceType := reflect.TypeOf(interfaceObject).Elem() + + if object == nil { + return Fail(t, fmt.Sprintf("Cannot check if nil implements %v", interfaceType), msgAndArgs...) + } + if !reflect.TypeOf(object).Implements(interfaceType) { + return Fail(t, fmt.Sprintf("%T must implement %v", object, interfaceType), msgAndArgs...) + } + + return true +} + +// IsType asserts that the specified objects are of the same type. +func IsType(t TestingT, expectedType interface{}, object interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + + if !ObjectsAreEqual(reflect.TypeOf(object), reflect.TypeOf(expectedType)) { + return Fail(t, fmt.Sprintf("Object expected to be of type %v, but was %v", reflect.TypeOf(expectedType), reflect.TypeOf(object)), msgAndArgs...) + } + + return true +} + +// Equal asserts that two objects are equal. +// +// assert.Equal(t, 123, 123) +// +// Pointer variable equality is determined based on the equality of the +// referenced values (as opposed to the memory addresses). Function equality +// cannot be determined and will always fail. +func Equal(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if err := validateEqualArgs(expected, actual); err != nil { + return Fail(t, fmt.Sprintf("Invalid operation: %#v == %#v (%s)", + expected, actual, err), msgAndArgs...) + } + + if !ObjectsAreEqual(expected, actual) { + diff := diff(expected, actual) + expected, actual = formatUnequalValues(expected, actual) + return Fail(t, fmt.Sprintf("Not equal: \n"+ + "expected: %s\n"+ + "actual : %s%s", expected, actual, diff), msgAndArgs...) + } + + return true + +} + +// Same asserts that two pointers reference the same object. +// +// assert.Same(t, ptr1, ptr2) +// +// Both arguments must be pointer variables. Pointer variable sameness is +// determined based on the equality of both type and value. +func Same(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + + expectedPtr, actualPtr := reflect.ValueOf(expected), reflect.ValueOf(actual) + if expectedPtr.Kind() != reflect.Ptr || actualPtr.Kind() != reflect.Ptr { + return Fail(t, "Invalid operation: both arguments must be pointers", msgAndArgs...) + } + + expectedType, actualType := reflect.TypeOf(expected), reflect.TypeOf(actual) + if expectedType != actualType { + return Fail(t, fmt.Sprintf("Pointer expected to be of type %v, but was %v", + expectedType, actualType), msgAndArgs...) + } + + if expected != actual { + return Fail(t, fmt.Sprintf("Not same: \n"+ + "expected: %p %#v\n"+ + "actual : %p %#v", expected, expected, actual, actual), msgAndArgs...) + } + + return true +} + +// formatUnequalValues takes two values of arbitrary types and returns string +// representations appropriate to be presented to the user. +// +// If the values are not of like type, the returned strings will be prefixed +// with the type name, and the value will be enclosed in parenthesis similar +// to a type conversion in the Go grammar. +func formatUnequalValues(expected, actual interface{}) (e string, a string) { + if reflect.TypeOf(expected) != reflect.TypeOf(actual) { + return fmt.Sprintf("%T(%#v)", expected, expected), + fmt.Sprintf("%T(%#v)", actual, actual) + } + + return fmt.Sprintf("%#v", expected), + fmt.Sprintf("%#v", actual) +} + +// EqualValues asserts that two objects are equal or convertable to the same types +// and equal. +// +// assert.EqualValues(t, uint32(123), int32(123)) +func EqualValues(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + + if !ObjectsAreEqualValues(expected, actual) { + diff := diff(expected, actual) + expected, actual = formatUnequalValues(expected, actual) + return Fail(t, fmt.Sprintf("Not equal: \n"+ + "expected: %s\n"+ + "actual : %s%s", expected, actual, diff), msgAndArgs...) + } + + return true + +} + +// Exactly asserts that two objects are equal in value and type. +// +// assert.Exactly(t, int32(123), int64(123)) +func Exactly(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + + aType := reflect.TypeOf(expected) + bType := reflect.TypeOf(actual) + + if aType != bType { + return Fail(t, fmt.Sprintf("Types expected to match exactly\n\t%v != %v", aType, bType), msgAndArgs...) + } + + return Equal(t, expected, actual, msgAndArgs...) + +} + +// NotNil asserts that the specified object is not nil. +// +// assert.NotNil(t, err) +func NotNil(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if !isNil(object) { + return true + } + return Fail(t, "Expected value not to be nil.", msgAndArgs...) +} + +// containsKind checks if a specified kind in the slice of kinds. +func containsKind(kinds []reflect.Kind, kind reflect.Kind) bool { + for i := 0; i < len(kinds); i++ { + if kind == kinds[i] { + return true + } + } + + return false +} + +// isNil checks if a specified object is nil or not, without Failing. +func isNil(object interface{}) bool { + if object == nil { + return true + } + + value := reflect.ValueOf(object) + kind := value.Kind() + isNilableKind := containsKind( + []reflect.Kind{ + reflect.Chan, reflect.Func, + reflect.Interface, reflect.Map, + reflect.Ptr, reflect.Slice}, + kind) + + if isNilableKind && value.IsNil() { + return true + } + + return false +} + +// Nil asserts that the specified object is nil. +// +// assert.Nil(t, err) +func Nil(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if isNil(object) { + return true + } + return Fail(t, fmt.Sprintf("Expected nil, but got: %#v", object), msgAndArgs...) +} + +// isEmpty gets whether the specified object is considered empty or not. +func isEmpty(object interface{}) bool { + + // get nil case out of the way + if object == nil { + return true + } + + objValue := reflect.ValueOf(object) + + switch objValue.Kind() { + // collection types are empty when they have no element + case reflect.Array, reflect.Chan, reflect.Map, reflect.Slice: + return objValue.Len() == 0 + // pointers are empty if nil or if the value they point to is empty + case reflect.Ptr: + if objValue.IsNil() { + return true + } + deref := objValue.Elem().Interface() + return isEmpty(deref) + // for all other types, compare against the zero value + default: + zero := reflect.Zero(objValue.Type()) + return reflect.DeepEqual(object, zero.Interface()) + } +} + +// Empty asserts that the specified object is empty. I.e. nil, "", false, 0 or either +// a slice or a channel with len == 0. +// +// assert.Empty(t, obj) +func Empty(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + + pass := isEmpty(object) + if !pass { + Fail(t, fmt.Sprintf("Should be empty, but was %v", object), msgAndArgs...) + } + + return pass + +} + +// NotEmpty asserts that the specified object is NOT empty. I.e. not nil, "", false, 0 or either +// a slice or a channel with len == 0. +// +// if assert.NotEmpty(t, obj) { +// assert.Equal(t, "two", obj[1]) +// } +func NotEmpty(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + + pass := !isEmpty(object) + if !pass { + Fail(t, fmt.Sprintf("Should NOT be empty, but was %v", object), msgAndArgs...) + } + + return pass + +} + +// getLen try to get length of object. +// return (false, 0) if impossible. +func getLen(x interface{}) (ok bool, length int) { + v := reflect.ValueOf(x) + defer func() { + if e := recover(); e != nil { + ok = false + } + }() + return true, v.Len() +} + +// Len asserts that the specified object has specific length. +// Len also fails if the object has a type that len() not accept. +// +// assert.Len(t, mySlice, 3) +func Len(t TestingT, object interface{}, length int, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + ok, l := getLen(object) + if !ok { + return Fail(t, fmt.Sprintf("\"%s\" could not be applied builtin len()", object), msgAndArgs...) + } + + if l != length { + return Fail(t, fmt.Sprintf("\"%s\" should have %d item(s), but has %d", object, length, l), msgAndArgs...) + } + return true +} + +// True asserts that the specified value is true. +// +// assert.True(t, myBool) +func True(t TestingT, value bool, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if h, ok := t.(interface { + Helper() + }); ok { + h.Helper() + } + + if value != true { + return Fail(t, "Should be true", msgAndArgs...) + } + + return true + +} + +// False asserts that the specified value is false. +// +// assert.False(t, myBool) +func False(t TestingT, value bool, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + + if value != false { + return Fail(t, "Should be false", msgAndArgs...) + } + + return true + +} + +// NotEqual asserts that the specified values are NOT equal. +// +// assert.NotEqual(t, obj1, obj2) +// +// Pointer variable equality is determined based on the equality of the +// referenced values (as opposed to the memory addresses). +func NotEqual(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if err := validateEqualArgs(expected, actual); err != nil { + return Fail(t, fmt.Sprintf("Invalid operation: %#v != %#v (%s)", + expected, actual, err), msgAndArgs...) + } + + if ObjectsAreEqual(expected, actual) { + return Fail(t, fmt.Sprintf("Should not be: %#v\n", actual), msgAndArgs...) + } + + return true + +} + +// containsElement try loop over the list check if the list includes the element. +// return (false, false) if impossible. +// return (true, false) if element was not found. +// return (true, true) if element was found. +func includeElement(list interface{}, element interface{}) (ok, found bool) { + + listValue := reflect.ValueOf(list) + listKind := reflect.TypeOf(list).Kind() + defer func() { + if e := recover(); e != nil { + ok = false + found = false + } + }() + + if listKind == reflect.String { + elementValue := reflect.ValueOf(element) + return true, strings.Contains(listValue.String(), elementValue.String()) + } + + if listKind == reflect.Map { + mapKeys := listValue.MapKeys() + for i := 0; i < len(mapKeys); i++ { + if ObjectsAreEqual(mapKeys[i].Interface(), element) { + return true, true + } + } + return true, false + } + + for i := 0; i < listValue.Len(); i++ { + if ObjectsAreEqual(listValue.Index(i).Interface(), element) { + return true, true + } + } + return true, false + +} + +// Contains asserts that the specified string, list(array, slice...) or map contains the +// specified substring or element. +// +// assert.Contains(t, "Hello World", "World") +// assert.Contains(t, ["Hello", "World"], "World") +// assert.Contains(t, {"Hello": "World"}, "Hello") +func Contains(t TestingT, s, contains interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + + ok, found := includeElement(s, contains) + if !ok { + return Fail(t, fmt.Sprintf("\"%s\" could not be applied builtin len()", s), msgAndArgs...) + } + if !found { + return Fail(t, fmt.Sprintf("\"%s\" does not contain \"%s\"", s, contains), msgAndArgs...) + } + + return true + +} + +// NotContains asserts that the specified string, list(array, slice...) or map does NOT contain the +// specified substring or element. +// +// assert.NotContains(t, "Hello World", "Earth") +// assert.NotContains(t, ["Hello", "World"], "Earth") +// assert.NotContains(t, {"Hello": "World"}, "Earth") +func NotContains(t TestingT, s, contains interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + + ok, found := includeElement(s, contains) + if !ok { + return Fail(t, fmt.Sprintf("\"%s\" could not be applied builtin len()", s), msgAndArgs...) + } + if found { + return Fail(t, fmt.Sprintf("\"%s\" should not contain \"%s\"", s, contains), msgAndArgs...) + } + + return true + +} + +// Subset asserts that the specified list(array, slice...) contains all +// elements given in the specified subset(array, slice...). +// +// assert.Subset(t, [1, 2, 3], [1, 2], "But [1, 2, 3] does contain [1, 2]") +func Subset(t TestingT, list, subset interface{}, msgAndArgs ...interface{}) (ok bool) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if subset == nil { + return true // we consider nil to be equal to the nil set + } + + subsetValue := reflect.ValueOf(subset) + defer func() { + if e := recover(); e != nil { + ok = false + } + }() + + listKind := reflect.TypeOf(list).Kind() + subsetKind := reflect.TypeOf(subset).Kind() + + if listKind != reflect.Array && listKind != reflect.Slice { + return Fail(t, fmt.Sprintf("%q has an unsupported type %s", list, listKind), msgAndArgs...) + } + + if subsetKind != reflect.Array && subsetKind != reflect.Slice { + return Fail(t, fmt.Sprintf("%q has an unsupported type %s", subset, subsetKind), msgAndArgs...) + } + + for i := 0; i < subsetValue.Len(); i++ { + element := subsetValue.Index(i).Interface() + ok, found := includeElement(list, element) + if !ok { + return Fail(t, fmt.Sprintf("\"%s\" could not be applied builtin len()", list), msgAndArgs...) + } + if !found { + return Fail(t, fmt.Sprintf("\"%s\" does not contain \"%s\"", list, element), msgAndArgs...) + } + } + + return true +} + +// NotSubset asserts that the specified list(array, slice...) contains not all +// elements given in the specified subset(array, slice...). +// +// assert.NotSubset(t, [1, 3, 4], [1, 2], "But [1, 3, 4] does not contain [1, 2]") +func NotSubset(t TestingT, list, subset interface{}, msgAndArgs ...interface{}) (ok bool) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if subset == nil { + return Fail(t, fmt.Sprintf("nil is the empty set which is a subset of every set"), msgAndArgs...) + } + + subsetValue := reflect.ValueOf(subset) + defer func() { + if e := recover(); e != nil { + ok = false + } + }() + + listKind := reflect.TypeOf(list).Kind() + subsetKind := reflect.TypeOf(subset).Kind() + + if listKind != reflect.Array && listKind != reflect.Slice { + return Fail(t, fmt.Sprintf("%q has an unsupported type %s", list, listKind), msgAndArgs...) + } + + if subsetKind != reflect.Array && subsetKind != reflect.Slice { + return Fail(t, fmt.Sprintf("%q has an unsupported type %s", subset, subsetKind), msgAndArgs...) + } + + for i := 0; i < subsetValue.Len(); i++ { + element := subsetValue.Index(i).Interface() + ok, found := includeElement(list, element) + if !ok { + return Fail(t, fmt.Sprintf("\"%s\" could not be applied builtin len()", list), msgAndArgs...) + } + if !found { + return true + } + } + + return Fail(t, fmt.Sprintf("%q is a subset of %q", subset, list), msgAndArgs...) +} + +// ElementsMatch asserts that the specified listA(array, slice...) is equal to specified +// listB(array, slice...) ignoring the order of the elements. If there are duplicate elements, +// the number of appearances of each of them in both lists should match. +// +// assert.ElementsMatch(t, [1, 3, 2, 3], [1, 3, 3, 2]) +func ElementsMatch(t TestingT, listA, listB interface{}, msgAndArgs ...interface{}) (ok bool) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if isEmpty(listA) && isEmpty(listB) { + return true + } + + aKind := reflect.TypeOf(listA).Kind() + bKind := reflect.TypeOf(listB).Kind() + + if aKind != reflect.Array && aKind != reflect.Slice { + return Fail(t, fmt.Sprintf("%q has an unsupported type %s", listA, aKind), msgAndArgs...) + } + + if bKind != reflect.Array && bKind != reflect.Slice { + return Fail(t, fmt.Sprintf("%q has an unsupported type %s", listB, bKind), msgAndArgs...) + } + + aValue := reflect.ValueOf(listA) + bValue := reflect.ValueOf(listB) + + aLen := aValue.Len() + bLen := bValue.Len() + + if aLen != bLen { + return Fail(t, fmt.Sprintf("lengths don't match: %d != %d", aLen, bLen), msgAndArgs...) + } + + // Mark indexes in bValue that we already used + visited := make([]bool, bLen) + for i := 0; i < aLen; i++ { + element := aValue.Index(i).Interface() + found := false + for j := 0; j < bLen; j++ { + if visited[j] { + continue + } + if ObjectsAreEqual(bValue.Index(j).Interface(), element) { + visited[j] = true + found = true + break + } + } + if !found { + return Fail(t, fmt.Sprintf("element %s appears more times in %s than in %s", element, aValue, bValue), msgAndArgs...) + } + } + + return true +} + +// Condition uses a Comparison to assert a complex condition. +func Condition(t TestingT, comp Comparison, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + result := comp() + if !result { + Fail(t, "Condition failed!", msgAndArgs...) + } + return result +} + +// PanicTestFunc defines a func that should be passed to the assert.Panics and assert.NotPanics +// methods, and represents a simple func that takes no arguments, and returns nothing. +type PanicTestFunc func() + +// didPanic returns true if the function passed to it panics. Otherwise, it returns false. +func didPanic(f PanicTestFunc) (bool, interface{}) { + + didPanic := false + var message interface{} + func() { + + defer func() { + if message = recover(); message != nil { + didPanic = true + } + }() + + // call the target function + f() + + }() + + return didPanic, message + +} + +// Panics asserts that the code inside the specified PanicTestFunc panics. +// +// assert.Panics(t, func(){ GoCrazy() }) +func Panics(t TestingT, f PanicTestFunc, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + + if funcDidPanic, panicValue := didPanic(f); !funcDidPanic { + return Fail(t, fmt.Sprintf("func %#v should panic\n\tPanic value:\t%#v", f, panicValue), msgAndArgs...) + } + + return true +} + +// PanicsWithValue asserts that the code inside the specified PanicTestFunc panics, and that +// the recovered panic value equals the expected panic value. +// +// assert.PanicsWithValue(t, "crazy error", func(){ GoCrazy() }) +func PanicsWithValue(t TestingT, expected interface{}, f PanicTestFunc, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + + funcDidPanic, panicValue := didPanic(f) + if !funcDidPanic { + return Fail(t, fmt.Sprintf("func %#v should panic\n\tPanic value:\t%#v", f, panicValue), msgAndArgs...) + } + if panicValue != expected { + return Fail(t, fmt.Sprintf("func %#v should panic with value:\t%#v\n\tPanic value:\t%#v", f, expected, panicValue), msgAndArgs...) + } + + return true +} + +// NotPanics asserts that the code inside the specified PanicTestFunc does NOT panic. +// +// assert.NotPanics(t, func(){ RemainCalm() }) +func NotPanics(t TestingT, f PanicTestFunc, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + + if funcDidPanic, panicValue := didPanic(f); funcDidPanic { + return Fail(t, fmt.Sprintf("func %#v should not panic\n\tPanic value:\t%v", f, panicValue), msgAndArgs...) + } + + return true +} + +// WithinDuration asserts that the two times are within duration delta of each other. +// +// assert.WithinDuration(t, time.Now(), time.Now(), 10*time.Second) +func WithinDuration(t TestingT, expected, actual time.Time, delta time.Duration, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + + dt := expected.Sub(actual) + if dt < -delta || dt > delta { + return Fail(t, fmt.Sprintf("Max difference between %v and %v allowed is %v, but difference was %v", expected, actual, delta, dt), msgAndArgs...) + } + + return true +} + +func toFloat(x interface{}) (float64, bool) { + var xf float64 + xok := true + + switch xn := x.(type) { + case uint8: + xf = float64(xn) + case uint16: + xf = float64(xn) + case uint32: + xf = float64(xn) + case uint64: + xf = float64(xn) + case int: + xf = float64(xn) + case int8: + xf = float64(xn) + case int16: + xf = float64(xn) + case int32: + xf = float64(xn) + case int64: + xf = float64(xn) + case float32: + xf = float64(xn) + case float64: + xf = float64(xn) + case time.Duration: + xf = float64(xn) + default: + xok = false + } + + return xf, xok +} + +// InDelta asserts that the two numerals are within delta of each other. +// +// assert.InDelta(t, math.Pi, (22 / 7.0), 0.01) +func InDelta(t TestingT, expected, actual interface{}, delta float64, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + + af, aok := toFloat(expected) + bf, bok := toFloat(actual) + + if !aok || !bok { + return Fail(t, fmt.Sprintf("Parameters must be numerical"), msgAndArgs...) + } + + if math.IsNaN(af) { + return Fail(t, fmt.Sprintf("Expected must not be NaN"), msgAndArgs...) + } + + if math.IsNaN(bf) { + return Fail(t, fmt.Sprintf("Expected %v with delta %v, but was NaN", expected, delta), msgAndArgs...) + } + + dt := af - bf + if dt < -delta || dt > delta { + return Fail(t, fmt.Sprintf("Max difference between %v and %v allowed is %v, but difference was %v", expected, actual, delta, dt), msgAndArgs...) + } + + return true +} + +// InDeltaSlice is the same as InDelta, except it compares two slices. +func InDeltaSlice(t TestingT, expected, actual interface{}, delta float64, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if expected == nil || actual == nil || + reflect.TypeOf(actual).Kind() != reflect.Slice || + reflect.TypeOf(expected).Kind() != reflect.Slice { + return Fail(t, fmt.Sprintf("Parameters must be slice"), msgAndArgs...) + } + + actualSlice := reflect.ValueOf(actual) + expectedSlice := reflect.ValueOf(expected) + + for i := 0; i < actualSlice.Len(); i++ { + result := InDelta(t, actualSlice.Index(i).Interface(), expectedSlice.Index(i).Interface(), delta, msgAndArgs...) + if !result { + return result + } + } + + return true +} + +// InDeltaMapValues is the same as InDelta, but it compares all values between two maps. Both maps must have exactly the same keys. +func InDeltaMapValues(t TestingT, expected, actual interface{}, delta float64, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if expected == nil || actual == nil || + reflect.TypeOf(actual).Kind() != reflect.Map || + reflect.TypeOf(expected).Kind() != reflect.Map { + return Fail(t, "Arguments must be maps", msgAndArgs...) + } + + expectedMap := reflect.ValueOf(expected) + actualMap := reflect.ValueOf(actual) + + if expectedMap.Len() != actualMap.Len() { + return Fail(t, "Arguments must have the same number of keys", msgAndArgs...) + } + + for _, k := range expectedMap.MapKeys() { + ev := expectedMap.MapIndex(k) + av := actualMap.MapIndex(k) + + if !ev.IsValid() { + return Fail(t, fmt.Sprintf("missing key %q in expected map", k), msgAndArgs...) + } + + if !av.IsValid() { + return Fail(t, fmt.Sprintf("missing key %q in actual map", k), msgAndArgs...) + } + + if !InDelta( + t, + ev.Interface(), + av.Interface(), + delta, + msgAndArgs..., + ) { + return false + } + } + + return true +} + +func calcRelativeError(expected, actual interface{}) (float64, error) { + af, aok := toFloat(expected) + if !aok { + return 0, fmt.Errorf("expected value %q cannot be converted to float", expected) + } + if af == 0 { + return 0, fmt.Errorf("expected value must have a value other than zero to calculate the relative error") + } + bf, bok := toFloat(actual) + if !bok { + return 0, fmt.Errorf("actual value %q cannot be converted to float", actual) + } + + return math.Abs(af-bf) / math.Abs(af), nil +} + +// InEpsilon asserts that expected and actual have a relative error less than epsilon +func InEpsilon(t TestingT, expected, actual interface{}, epsilon float64, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + actualEpsilon, err := calcRelativeError(expected, actual) + if err != nil { + return Fail(t, err.Error(), msgAndArgs...) + } + if actualEpsilon > epsilon { + return Fail(t, fmt.Sprintf("Relative error is too high: %#v (expected)\n"+ + " < %#v (actual)", epsilon, actualEpsilon), msgAndArgs...) + } + + return true +} + +// InEpsilonSlice is the same as InEpsilon, except it compares each value from two slices. +func InEpsilonSlice(t TestingT, expected, actual interface{}, epsilon float64, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if expected == nil || actual == nil || + reflect.TypeOf(actual).Kind() != reflect.Slice || + reflect.TypeOf(expected).Kind() != reflect.Slice { + return Fail(t, fmt.Sprintf("Parameters must be slice"), msgAndArgs...) + } + + actualSlice := reflect.ValueOf(actual) + expectedSlice := reflect.ValueOf(expected) + + for i := 0; i < actualSlice.Len(); i++ { + result := InEpsilon(t, actualSlice.Index(i).Interface(), expectedSlice.Index(i).Interface(), epsilon) + if !result { + return result + } + } + + return true +} + +/* + Errors +*/ + +// NoError asserts that a function returned no error (i.e. `nil`). +// +// actualObj, err := SomeFunction() +// if assert.NoError(t, err) { +// assert.Equal(t, expectedObj, actualObj) +// } +func NoError(t TestingT, err error, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if err != nil { + return Fail(t, fmt.Sprintf("Received unexpected error:\n%+v", err), msgAndArgs...) + } + + return true +} + +// Error asserts that a function returned an error (i.e. not `nil`). +// +// actualObj, err := SomeFunction() +// if assert.Error(t, err) { +// assert.Equal(t, expectedError, err) +// } +func Error(t TestingT, err error, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + + if err == nil { + return Fail(t, "An error is expected but got nil.", msgAndArgs...) + } + + return true +} + +// EqualError asserts that a function returned an error (i.e. not `nil`) +// and that it is equal to the provided error. +// +// actualObj, err := SomeFunction() +// assert.EqualError(t, err, expectedErrorString) +func EqualError(t TestingT, theError error, errString string, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if !Error(t, theError, msgAndArgs...) { + return false + } + expected := errString + actual := theError.Error() + // don't need to use deep equals here, we know they are both strings + if expected != actual { + return Fail(t, fmt.Sprintf("Error message not equal:\n"+ + "expected: %q\n"+ + "actual : %q", expected, actual), msgAndArgs...) + } + return true +} + +// matchRegexp return true if a specified regexp matches a string. +func matchRegexp(rx interface{}, str interface{}) bool { + + var r *regexp.Regexp + if rr, ok := rx.(*regexp.Regexp); ok { + r = rr + } else { + r = regexp.MustCompile(fmt.Sprint(rx)) + } + + return (r.FindStringIndex(fmt.Sprint(str)) != nil) + +} + +// Regexp asserts that a specified regexp matches a string. +// +// assert.Regexp(t, regexp.MustCompile("start"), "it's starting") +// assert.Regexp(t, "start...$", "it's not starting") +func Regexp(t TestingT, rx interface{}, str interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + + match := matchRegexp(rx, str) + + if !match { + Fail(t, fmt.Sprintf("Expect \"%v\" to match \"%v\"", str, rx), msgAndArgs...) + } + + return match +} + +// NotRegexp asserts that a specified regexp does not match a string. +// +// assert.NotRegexp(t, regexp.MustCompile("starts"), "it's starting") +// assert.NotRegexp(t, "^start", "it's not starting") +func NotRegexp(t TestingT, rx interface{}, str interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + match := matchRegexp(rx, str) + + if match { + Fail(t, fmt.Sprintf("Expect \"%v\" to NOT match \"%v\"", str, rx), msgAndArgs...) + } + + return !match + +} + +// Zero asserts that i is the zero value for its type. +func Zero(t TestingT, i interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if i != nil && !reflect.DeepEqual(i, reflect.Zero(reflect.TypeOf(i)).Interface()) { + return Fail(t, fmt.Sprintf("Should be zero, but was %v", i), msgAndArgs...) + } + return true +} + +// NotZero asserts that i is not the zero value for its type. +func NotZero(t TestingT, i interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if i == nil || reflect.DeepEqual(i, reflect.Zero(reflect.TypeOf(i)).Interface()) { + return Fail(t, fmt.Sprintf("Should not be zero, but was %v", i), msgAndArgs...) + } + return true +} + +// FileExists checks whether a file exists in the given path. It also fails if the path points to a directory or there is an error when trying to check the file. +func FileExists(t TestingT, path string, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + info, err := os.Lstat(path) + if err != nil { + if os.IsNotExist(err) { + return Fail(t, fmt.Sprintf("unable to find file %q", path), msgAndArgs...) + } + return Fail(t, fmt.Sprintf("error when running os.Lstat(%q): %s", path, err), msgAndArgs...) + } + if info.IsDir() { + return Fail(t, fmt.Sprintf("%q is a directory", path), msgAndArgs...) + } + return true +} + +// DirExists checks whether a directory exists in the given path. It also fails if the path is a file rather a directory or there is an error checking whether it exists. +func DirExists(t TestingT, path string, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + info, err := os.Lstat(path) + if err != nil { + if os.IsNotExist(err) { + return Fail(t, fmt.Sprintf("unable to find file %q", path), msgAndArgs...) + } + return Fail(t, fmt.Sprintf("error when running os.Lstat(%q): %s", path, err), msgAndArgs...) + } + if !info.IsDir() { + return Fail(t, fmt.Sprintf("%q is a file", path), msgAndArgs...) + } + return true +} + +// JSONEq asserts that two JSON strings are equivalent. +// +// assert.JSONEq(t, `{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`) +func JSONEq(t TestingT, expected string, actual string, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + var expectedJSONAsInterface, actualJSONAsInterface interface{} + + if err := json.Unmarshal([]byte(expected), &expectedJSONAsInterface); err != nil { + return Fail(t, fmt.Sprintf("Expected value ('%s') is not valid json.\nJSON parsing error: '%s'", expected, err.Error()), msgAndArgs...) + } + + if err := json.Unmarshal([]byte(actual), &actualJSONAsInterface); err != nil { + return Fail(t, fmt.Sprintf("Input ('%s') needs to be valid json.\nJSON parsing error: '%s'", actual, err.Error()), msgAndArgs...) + } + + return Equal(t, expectedJSONAsInterface, actualJSONAsInterface, msgAndArgs...) +} + +// YAMLEq asserts that two YAML strings are equivalent. +func YAMLEq(t TestingT, expected string, actual string, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + var expectedYAMLAsInterface, actualYAMLAsInterface interface{} + + if err := yaml.Unmarshal([]byte(expected), &expectedYAMLAsInterface); err != nil { + return Fail(t, fmt.Sprintf("Expected value ('%s') is not valid yaml.\nYAML parsing error: '%s'", expected, err.Error()), msgAndArgs...) + } + + if err := yaml.Unmarshal([]byte(actual), &actualYAMLAsInterface); err != nil { + return Fail(t, fmt.Sprintf("Input ('%s') needs to be valid yaml.\nYAML error: '%s'", actual, err.Error()), msgAndArgs...) + } + + return Equal(t, expectedYAMLAsInterface, actualYAMLAsInterface, msgAndArgs...) +} + +func typeAndKind(v interface{}) (reflect.Type, reflect.Kind) { + t := reflect.TypeOf(v) + k := t.Kind() + + if k == reflect.Ptr { + t = t.Elem() + k = t.Kind() + } + return t, k +} + +// diff returns a diff of both values as long as both are of the same type and +// are a struct, map, slice, array or string. Otherwise it returns an empty string. +func diff(expected interface{}, actual interface{}) string { + if expected == nil || actual == nil { + return "" + } + + et, ek := typeAndKind(expected) + at, _ := typeAndKind(actual) + + if et != at { + return "" + } + + if ek != reflect.Struct && ek != reflect.Map && ek != reflect.Slice && ek != reflect.Array && ek != reflect.String { + return "" + } + + var e, a string + if et != reflect.TypeOf("") { + e = spewConfig.Sdump(expected) + a = spewConfig.Sdump(actual) + } else { + e = reflect.ValueOf(expected).String() + a = reflect.ValueOf(actual).String() + } + + diff, _ := difflib.GetUnifiedDiffString(difflib.UnifiedDiff{ + A: difflib.SplitLines(e), + B: difflib.SplitLines(a), + FromFile: "Expected", + FromDate: "", + ToFile: "Actual", + ToDate: "", + Context: 1, + }) + + return "\n\nDiff:\n" + diff +} + +// validateEqualArgs checks whether provided arguments can be safely used in the +// Equal/NotEqual functions. +func validateEqualArgs(expected, actual interface{}) error { + if isFunction(expected) || isFunction(actual) { + return errors.New("cannot take func type as argument") + } + return nil +} + +func isFunction(arg interface{}) bool { + if arg == nil { + return false + } + return reflect.TypeOf(arg).Kind() == reflect.Func +} + +var spewConfig = spew.ConfigState{ + Indent: " ", + DisablePointerAddresses: true, + DisableCapacities: true, + SortKeys: true, +} + +type tHelper interface { + Helper() +} + +// Eventually asserts that given condition will be met in waitFor time, +// periodically checking target function each tick. +// +// assert.Eventually(t, func() bool { return true; }, time.Second, 10*time.Millisecond) +func Eventually(t TestingT, condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + + timer := time.NewTimer(waitFor) + ticker := time.NewTicker(tick) + checkPassed := make(chan bool) + defer timer.Stop() + defer ticker.Stop() + defer close(checkPassed) + for { + select { + case <-timer.C: + return Fail(t, "Condition never satisfied", msgAndArgs...) + case result := <-checkPassed: + if result { + return true + } + case <-ticker.C: + go func() { + checkPassed <- condition() + }() + } + } +} diff --git a/vendor/github.com/stretchr/testify/assert/doc.go b/vendor/github.com/stretchr/testify/assert/doc.go new file mode 100644 index 000000000..c9dccc4d6 --- /dev/null +++ b/vendor/github.com/stretchr/testify/assert/doc.go @@ -0,0 +1,45 @@ +// Package assert provides a set of comprehensive testing tools for use with the normal Go testing system. +// +// Example Usage +// +// The following is a complete example using assert in a standard test function: +// import ( +// "testing" +// "github.com/stretchr/testify/assert" +// ) +// +// func TestSomething(t *testing.T) { +// +// var a string = "Hello" +// var b string = "Hello" +// +// assert.Equal(t, a, b, "The two words should be the same.") +// +// } +// +// if you assert many times, use the format below: +// +// import ( +// "testing" +// "github.com/stretchr/testify/assert" +// ) +// +// func TestSomething(t *testing.T) { +// assert := assert.New(t) +// +// var a string = "Hello" +// var b string = "Hello" +// +// assert.Equal(a, b, "The two words should be the same.") +// } +// +// Assertions +// +// Assertions allow you to easily write test code, and are global funcs in the `assert` package. +// All assertion functions take, as the first argument, the `*testing.T` object provided by the +// testing framework. This allows the assertion funcs to write the failings and other details to +// the correct place. +// +// Every assertion function also takes an optional string message as the final argument, +// allowing custom error messages to be appended to the message the assertion method outputs. +package assert diff --git a/vendor/github.com/stretchr/testify/assert/errors.go b/vendor/github.com/stretchr/testify/assert/errors.go new file mode 100644 index 000000000..ac9dc9d1d --- /dev/null +++ b/vendor/github.com/stretchr/testify/assert/errors.go @@ -0,0 +1,10 @@ +package assert + +import ( + "errors" +) + +// AnError is an error instance useful for testing. If the code does not care +// about error specifics, and only needs to return the error for example, this +// error should be used to make the test code more readable. +var AnError = errors.New("assert.AnError general error for testing") diff --git a/vendor/github.com/stretchr/testify/assert/forward_assertions.go b/vendor/github.com/stretchr/testify/assert/forward_assertions.go new file mode 100644 index 000000000..9ad56851d --- /dev/null +++ b/vendor/github.com/stretchr/testify/assert/forward_assertions.go @@ -0,0 +1,16 @@ +package assert + +// Assertions provides assertion methods around the +// TestingT interface. +type Assertions struct { + t TestingT +} + +// New makes a new Assertions object for the specified TestingT. +func New(t TestingT) *Assertions { + return &Assertions{ + t: t, + } +} + +//go:generate go run ../_codegen/main.go -output-package=assert -template=assertion_forward.go.tmpl -include-format-funcs diff --git a/vendor/github.com/stretchr/testify/assert/http_assertions.go b/vendor/github.com/stretchr/testify/assert/http_assertions.go new file mode 100644 index 000000000..df46fa777 --- /dev/null +++ b/vendor/github.com/stretchr/testify/assert/http_assertions.go @@ -0,0 +1,143 @@ +package assert + +import ( + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "strings" +) + +// httpCode is a helper that returns HTTP code of the response. It returns -1 and +// an error if building a new request fails. +func httpCode(handler http.HandlerFunc, method, url string, values url.Values) (int, error) { + w := httptest.NewRecorder() + req, err := http.NewRequest(method, url, nil) + if err != nil { + return -1, err + } + req.URL.RawQuery = values.Encode() + handler(w, req) + return w.Code, nil +} + +// HTTPSuccess asserts that a specified handler returns a success status code. +// +// assert.HTTPSuccess(t, myHandler, "POST", "http://www.google.com", nil) +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPSuccess(t TestingT, handler http.HandlerFunc, method, url string, values url.Values, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + code, err := httpCode(handler, method, url, values) + if err != nil { + Fail(t, fmt.Sprintf("Failed to build test request, got error: %s", err)) + return false + } + + isSuccessCode := code >= http.StatusOK && code <= http.StatusPartialContent + if !isSuccessCode { + Fail(t, fmt.Sprintf("Expected HTTP success status code for %q but received %d", url+"?"+values.Encode(), code)) + } + + return isSuccessCode +} + +// HTTPRedirect asserts that a specified handler returns a redirect status code. +// +// assert.HTTPRedirect(t, myHandler, "GET", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPRedirect(t TestingT, handler http.HandlerFunc, method, url string, values url.Values, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + code, err := httpCode(handler, method, url, values) + if err != nil { + Fail(t, fmt.Sprintf("Failed to build test request, got error: %s", err)) + return false + } + + isRedirectCode := code >= http.StatusMultipleChoices && code <= http.StatusTemporaryRedirect + if !isRedirectCode { + Fail(t, fmt.Sprintf("Expected HTTP redirect status code for %q but received %d", url+"?"+values.Encode(), code)) + } + + return isRedirectCode +} + +// HTTPError asserts that a specified handler returns an error status code. +// +// assert.HTTPError(t, myHandler, "POST", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPError(t TestingT, handler http.HandlerFunc, method, url string, values url.Values, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + code, err := httpCode(handler, method, url, values) + if err != nil { + Fail(t, fmt.Sprintf("Failed to build test request, got error: %s", err)) + return false + } + + isErrorCode := code >= http.StatusBadRequest + if !isErrorCode { + Fail(t, fmt.Sprintf("Expected HTTP error status code for %q but received %d", url+"?"+values.Encode(), code)) + } + + return isErrorCode +} + +// HTTPBody is a helper that returns HTTP body of the response. It returns +// empty string if building a new request fails. +func HTTPBody(handler http.HandlerFunc, method, url string, values url.Values) string { + w := httptest.NewRecorder() + req, err := http.NewRequest(method, url+"?"+values.Encode(), nil) + if err != nil { + return "" + } + handler(w, req) + return w.Body.String() +} + +// HTTPBodyContains asserts that a specified handler returns a +// body that contains a string. +// +// assert.HTTPBodyContains(t, myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky") +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPBodyContains(t TestingT, handler http.HandlerFunc, method, url string, values url.Values, str interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + body := HTTPBody(handler, method, url, values) + + contains := strings.Contains(body, fmt.Sprint(str)) + if !contains { + Fail(t, fmt.Sprintf("Expected response body for \"%s\" to contain \"%s\" but found \"%s\"", url+"?"+values.Encode(), str, body)) + } + + return contains +} + +// HTTPBodyNotContains asserts that a specified handler returns a +// body that does not contain a string. +// +// assert.HTTPBodyNotContains(t, myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky") +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPBodyNotContains(t TestingT, handler http.HandlerFunc, method, url string, values url.Values, str interface{}, msgAndArgs ...interface{}) bool { + if h, ok := t.(tHelper); ok { + h.Helper() + } + body := HTTPBody(handler, method, url, values) + + contains := strings.Contains(body, fmt.Sprint(str)) + if contains { + Fail(t, fmt.Sprintf("Expected response body for \"%s\" to NOT contain \"%s\" but found \"%s\"", url+"?"+values.Encode(), str, body)) + } + + return !contains +} diff --git a/vendor/github.com/stretchr/testify/go.mod b/vendor/github.com/stretchr/testify/go.mod new file mode 100644 index 000000000..50536488c --- /dev/null +++ b/vendor/github.com/stretchr/testify/go.mod @@ -0,0 +1,8 @@ +module github.com/stretchr/testify + +require ( + github.com/davecgh/go-spew v1.1.0 + github.com/pmezard/go-difflib v1.0.0 + github.com/stretchr/objx v0.1.0 + gopkg.in/yaml.v2 v2.2.2 +) diff --git a/vendor/github.com/stretchr/testify/require/doc.go b/vendor/github.com/stretchr/testify/require/doc.go new file mode 100644 index 000000000..169de3922 --- /dev/null +++ b/vendor/github.com/stretchr/testify/require/doc.go @@ -0,0 +1,28 @@ +// Package require implements the same assertions as the `assert` package but +// stops test execution when a test fails. +// +// Example Usage +// +// The following is a complete example using require in a standard test function: +// import ( +// "testing" +// "github.com/stretchr/testify/require" +// ) +// +// func TestSomething(t *testing.T) { +// +// var a string = "Hello" +// var b string = "Hello" +// +// require.Equal(t, a, b, "The two words should be the same.") +// +// } +// +// Assertions +// +// The `require` package have same global functions as in the `assert` package, +// but instead of returning a boolean result they call `t.FailNow()`. +// +// Every assertion function also takes an optional string message as the final argument, +// allowing custom error messages to be appended to the message the assertion method outputs. +package require diff --git a/vendor/github.com/stretchr/testify/require/forward_requirements.go b/vendor/github.com/stretchr/testify/require/forward_requirements.go new file mode 100644 index 000000000..ac71d4058 --- /dev/null +++ b/vendor/github.com/stretchr/testify/require/forward_requirements.go @@ -0,0 +1,16 @@ +package require + +// Assertions provides assertion methods around the +// TestingT interface. +type Assertions struct { + t TestingT +} + +// New makes a new Assertions object for the specified TestingT. +func New(t TestingT) *Assertions { + return &Assertions{ + t: t, + } +} + +//go:generate go run ../_codegen/main.go -output-package=require -template=require_forward.go.tmpl -include-format-funcs diff --git a/vendor/github.com/stretchr/testify/require/require.go b/vendor/github.com/stretchr/testify/require/require.go new file mode 100644 index 000000000..c5903f5db --- /dev/null +++ b/vendor/github.com/stretchr/testify/require/require.go @@ -0,0 +1,1433 @@ +/* +* CODE GENERATED AUTOMATICALLY WITH github.com/stretchr/testify/_codegen +* THIS FILE MUST NOT BE EDITED BY HAND + */ + +package require + +import ( + assert "github.com/stretchr/testify/assert" + http "net/http" + url "net/url" + time "time" +) + +// Condition uses a Comparison to assert a complex condition. +func Condition(t TestingT, comp assert.Comparison, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Condition(t, comp, msgAndArgs...) { + return + } + t.FailNow() +} + +// Conditionf uses a Comparison to assert a complex condition. +func Conditionf(t TestingT, comp assert.Comparison, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Conditionf(t, comp, msg, args...) { + return + } + t.FailNow() +} + +// Contains asserts that the specified string, list(array, slice...) or map contains the +// specified substring or element. +// +// assert.Contains(t, "Hello World", "World") +// assert.Contains(t, ["Hello", "World"], "World") +// assert.Contains(t, {"Hello": "World"}, "Hello") +func Contains(t TestingT, s interface{}, contains interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Contains(t, s, contains, msgAndArgs...) { + return + } + t.FailNow() +} + +// Containsf asserts that the specified string, list(array, slice...) or map contains the +// specified substring or element. +// +// assert.Containsf(t, "Hello World", "World", "error message %s", "formatted") +// assert.Containsf(t, ["Hello", "World"], "World", "error message %s", "formatted") +// assert.Containsf(t, {"Hello": "World"}, "Hello", "error message %s", "formatted") +func Containsf(t TestingT, s interface{}, contains interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Containsf(t, s, contains, msg, args...) { + return + } + t.FailNow() +} + +// DirExists checks whether a directory exists in the given path. It also fails if the path is a file rather a directory or there is an error checking whether it exists. +func DirExists(t TestingT, path string, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.DirExists(t, path, msgAndArgs...) { + return + } + t.FailNow() +} + +// DirExistsf checks whether a directory exists in the given path. It also fails if the path is a file rather a directory or there is an error checking whether it exists. +func DirExistsf(t TestingT, path string, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.DirExistsf(t, path, msg, args...) { + return + } + t.FailNow() +} + +// ElementsMatch asserts that the specified listA(array, slice...) is equal to specified +// listB(array, slice...) ignoring the order of the elements. If there are duplicate elements, +// the number of appearances of each of them in both lists should match. +// +// assert.ElementsMatch(t, [1, 3, 2, 3], [1, 3, 3, 2]) +func ElementsMatch(t TestingT, listA interface{}, listB interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.ElementsMatch(t, listA, listB, msgAndArgs...) { + return + } + t.FailNow() +} + +// ElementsMatchf asserts that the specified listA(array, slice...) is equal to specified +// listB(array, slice...) ignoring the order of the elements. If there are duplicate elements, +// the number of appearances of each of them in both lists should match. +// +// assert.ElementsMatchf(t, [1, 3, 2, 3], [1, 3, 3, 2], "error message %s", "formatted") +func ElementsMatchf(t TestingT, listA interface{}, listB interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.ElementsMatchf(t, listA, listB, msg, args...) { + return + } + t.FailNow() +} + +// Empty asserts that the specified object is empty. I.e. nil, "", false, 0 or either +// a slice or a channel with len == 0. +// +// assert.Empty(t, obj) +func Empty(t TestingT, object interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Empty(t, object, msgAndArgs...) { + return + } + t.FailNow() +} + +// Emptyf asserts that the specified object is empty. I.e. nil, "", false, 0 or either +// a slice or a channel with len == 0. +// +// assert.Emptyf(t, obj, "error message %s", "formatted") +func Emptyf(t TestingT, object interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Emptyf(t, object, msg, args...) { + return + } + t.FailNow() +} + +// Equal asserts that two objects are equal. +// +// assert.Equal(t, 123, 123) +// +// Pointer variable equality is determined based on the equality of the +// referenced values (as opposed to the memory addresses). Function equality +// cannot be determined and will always fail. +func Equal(t TestingT, expected interface{}, actual interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Equal(t, expected, actual, msgAndArgs...) { + return + } + t.FailNow() +} + +// EqualError asserts that a function returned an error (i.e. not `nil`) +// and that it is equal to the provided error. +// +// actualObj, err := SomeFunction() +// assert.EqualError(t, err, expectedErrorString) +func EqualError(t TestingT, theError error, errString string, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.EqualError(t, theError, errString, msgAndArgs...) { + return + } + t.FailNow() +} + +// EqualErrorf asserts that a function returned an error (i.e. not `nil`) +// and that it is equal to the provided error. +// +// actualObj, err := SomeFunction() +// assert.EqualErrorf(t, err, expectedErrorString, "error message %s", "formatted") +func EqualErrorf(t TestingT, theError error, errString string, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.EqualErrorf(t, theError, errString, msg, args...) { + return + } + t.FailNow() +} + +// EqualValues asserts that two objects are equal or convertable to the same types +// and equal. +// +// assert.EqualValues(t, uint32(123), int32(123)) +func EqualValues(t TestingT, expected interface{}, actual interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.EqualValues(t, expected, actual, msgAndArgs...) { + return + } + t.FailNow() +} + +// EqualValuesf asserts that two objects are equal or convertable to the same types +// and equal. +// +// assert.EqualValuesf(t, uint32(123, "error message %s", "formatted"), int32(123)) +func EqualValuesf(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.EqualValuesf(t, expected, actual, msg, args...) { + return + } + t.FailNow() +} + +// Equalf asserts that two objects are equal. +// +// assert.Equalf(t, 123, 123, "error message %s", "formatted") +// +// Pointer variable equality is determined based on the equality of the +// referenced values (as opposed to the memory addresses). Function equality +// cannot be determined and will always fail. +func Equalf(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Equalf(t, expected, actual, msg, args...) { + return + } + t.FailNow() +} + +// Error asserts that a function returned an error (i.e. not `nil`). +// +// actualObj, err := SomeFunction() +// if assert.Error(t, err) { +// assert.Equal(t, expectedError, err) +// } +func Error(t TestingT, err error, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Error(t, err, msgAndArgs...) { + return + } + t.FailNow() +} + +// Errorf asserts that a function returned an error (i.e. not `nil`). +// +// actualObj, err := SomeFunction() +// if assert.Errorf(t, err, "error message %s", "formatted") { +// assert.Equal(t, expectedErrorf, err) +// } +func Errorf(t TestingT, err error, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Errorf(t, err, msg, args...) { + return + } + t.FailNow() +} + +// Eventually asserts that given condition will be met in waitFor time, +// periodically checking target function each tick. +// +// assert.Eventually(t, func() bool { return true; }, time.Second, 10*time.Millisecond) +func Eventually(t TestingT, condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) { + if assert.Eventually(t, condition, waitFor, tick, msgAndArgs...) { + return + } + if h, ok := t.(tHelper); ok { + h.Helper() + } + t.FailNow() +} + +// Eventuallyf asserts that given condition will be met in waitFor time, +// periodically checking target function each tick. +// +// assert.Eventuallyf(t, func() bool { return true; }, time.Second, 10*time.Millisecond, "error message %s", "formatted") +func Eventuallyf(t TestingT, condition func() bool, waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) { + if assert.Eventuallyf(t, condition, waitFor, tick, msg, args...) { + return + } + if h, ok := t.(tHelper); ok { + h.Helper() + } + t.FailNow() +} + +// Exactly asserts that two objects are equal in value and type. +// +// assert.Exactly(t, int32(123), int64(123)) +func Exactly(t TestingT, expected interface{}, actual interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Exactly(t, expected, actual, msgAndArgs...) { + return + } + t.FailNow() +} + +// Exactlyf asserts that two objects are equal in value and type. +// +// assert.Exactlyf(t, int32(123, "error message %s", "formatted"), int64(123)) +func Exactlyf(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Exactlyf(t, expected, actual, msg, args...) { + return + } + t.FailNow() +} + +// Fail reports a failure through +func Fail(t TestingT, failureMessage string, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Fail(t, failureMessage, msgAndArgs...) { + return + } + t.FailNow() +} + +// FailNow fails test +func FailNow(t TestingT, failureMessage string, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.FailNow(t, failureMessage, msgAndArgs...) { + return + } + t.FailNow() +} + +// FailNowf fails test +func FailNowf(t TestingT, failureMessage string, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.FailNowf(t, failureMessage, msg, args...) { + return + } + t.FailNow() +} + +// Failf reports a failure through +func Failf(t TestingT, failureMessage string, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Failf(t, failureMessage, msg, args...) { + return + } + t.FailNow() +} + +// False asserts that the specified value is false. +// +// assert.False(t, myBool) +func False(t TestingT, value bool, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.False(t, value, msgAndArgs...) { + return + } + t.FailNow() +} + +// Falsef asserts that the specified value is false. +// +// assert.Falsef(t, myBool, "error message %s", "formatted") +func Falsef(t TestingT, value bool, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Falsef(t, value, msg, args...) { + return + } + t.FailNow() +} + +// FileExists checks whether a file exists in the given path. It also fails if the path points to a directory or there is an error when trying to check the file. +func FileExists(t TestingT, path string, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.FileExists(t, path, msgAndArgs...) { + return + } + t.FailNow() +} + +// FileExistsf checks whether a file exists in the given path. It also fails if the path points to a directory or there is an error when trying to check the file. +func FileExistsf(t TestingT, path string, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.FileExistsf(t, path, msg, args...) { + return + } + t.FailNow() +} + +// Greater asserts that the first element is greater than the second +// +// assert.Greater(t, 2, 1) +// assert.Greater(t, float64(2), float64(1)) +// assert.Greater(t, "b", "a") +func Greater(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Greater(t, e1, e2, msgAndArgs...) { + return + } + t.FailNow() +} + +// GreaterOrEqual asserts that the first element is greater than or equal to the second +// +// assert.GreaterOrEqual(t, 2, 1) +// assert.GreaterOrEqual(t, 2, 2) +// assert.GreaterOrEqual(t, "b", "a") +// assert.GreaterOrEqual(t, "b", "b") +func GreaterOrEqual(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.GreaterOrEqual(t, e1, e2, msgAndArgs...) { + return + } + t.FailNow() +} + +// GreaterOrEqualf asserts that the first element is greater than or equal to the second +// +// assert.GreaterOrEqualf(t, 2, 1, "error message %s", "formatted") +// assert.GreaterOrEqualf(t, 2, 2, "error message %s", "formatted") +// assert.GreaterOrEqualf(t, "b", "a", "error message %s", "formatted") +// assert.GreaterOrEqualf(t, "b", "b", "error message %s", "formatted") +func GreaterOrEqualf(t TestingT, e1 interface{}, e2 interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.GreaterOrEqualf(t, e1, e2, msg, args...) { + return + } + t.FailNow() +} + +// Greaterf asserts that the first element is greater than the second +// +// assert.Greaterf(t, 2, 1, "error message %s", "formatted") +// assert.Greaterf(t, float64(2, "error message %s", "formatted"), float64(1)) +// assert.Greaterf(t, "b", "a", "error message %s", "formatted") +func Greaterf(t TestingT, e1 interface{}, e2 interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Greaterf(t, e1, e2, msg, args...) { + return + } + t.FailNow() +} + +// HTTPBodyContains asserts that a specified handler returns a +// body that contains a string. +// +// assert.HTTPBodyContains(t, myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky") +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPBodyContains(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.HTTPBodyContains(t, handler, method, url, values, str, msgAndArgs...) { + return + } + t.FailNow() +} + +// HTTPBodyContainsf asserts that a specified handler returns a +// body that contains a string. +// +// assert.HTTPBodyContainsf(t, myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky", "error message %s", "formatted") +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPBodyContainsf(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.HTTPBodyContainsf(t, handler, method, url, values, str, msg, args...) { + return + } + t.FailNow() +} + +// HTTPBodyNotContains asserts that a specified handler returns a +// body that does not contain a string. +// +// assert.HTTPBodyNotContains(t, myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky") +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPBodyNotContains(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.HTTPBodyNotContains(t, handler, method, url, values, str, msgAndArgs...) { + return + } + t.FailNow() +} + +// HTTPBodyNotContainsf asserts that a specified handler returns a +// body that does not contain a string. +// +// assert.HTTPBodyNotContainsf(t, myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky", "error message %s", "formatted") +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPBodyNotContainsf(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.HTTPBodyNotContainsf(t, handler, method, url, values, str, msg, args...) { + return + } + t.FailNow() +} + +// HTTPError asserts that a specified handler returns an error status code. +// +// assert.HTTPError(t, myHandler, "POST", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPError(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.HTTPError(t, handler, method, url, values, msgAndArgs...) { + return + } + t.FailNow() +} + +// HTTPErrorf asserts that a specified handler returns an error status code. +// +// assert.HTTPErrorf(t, myHandler, "POST", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// +// Returns whether the assertion was successful (true, "error message %s", "formatted") or not (false). +func HTTPErrorf(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.HTTPErrorf(t, handler, method, url, values, msg, args...) { + return + } + t.FailNow() +} + +// HTTPRedirect asserts that a specified handler returns a redirect status code. +// +// assert.HTTPRedirect(t, myHandler, "GET", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPRedirect(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.HTTPRedirect(t, handler, method, url, values, msgAndArgs...) { + return + } + t.FailNow() +} + +// HTTPRedirectf asserts that a specified handler returns a redirect status code. +// +// assert.HTTPRedirectf(t, myHandler, "GET", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// +// Returns whether the assertion was successful (true, "error message %s", "formatted") or not (false). +func HTTPRedirectf(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.HTTPRedirectf(t, handler, method, url, values, msg, args...) { + return + } + t.FailNow() +} + +// HTTPSuccess asserts that a specified handler returns a success status code. +// +// assert.HTTPSuccess(t, myHandler, "POST", "http://www.google.com", nil) +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPSuccess(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.HTTPSuccess(t, handler, method, url, values, msgAndArgs...) { + return + } + t.FailNow() +} + +// HTTPSuccessf asserts that a specified handler returns a success status code. +// +// assert.HTTPSuccessf(t, myHandler, "POST", "http://www.google.com", nil, "error message %s", "formatted") +// +// Returns whether the assertion was successful (true) or not (false). +func HTTPSuccessf(t TestingT, handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.HTTPSuccessf(t, handler, method, url, values, msg, args...) { + return + } + t.FailNow() +} + +// Implements asserts that an object is implemented by the specified interface. +// +// assert.Implements(t, (*MyInterface)(nil), new(MyObject)) +func Implements(t TestingT, interfaceObject interface{}, object interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Implements(t, interfaceObject, object, msgAndArgs...) { + return + } + t.FailNow() +} + +// Implementsf asserts that an object is implemented by the specified interface. +// +// assert.Implementsf(t, (*MyInterface, "error message %s", "formatted")(nil), new(MyObject)) +func Implementsf(t TestingT, interfaceObject interface{}, object interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Implementsf(t, interfaceObject, object, msg, args...) { + return + } + t.FailNow() +} + +// InDelta asserts that the two numerals are within delta of each other. +// +// assert.InDelta(t, math.Pi, (22 / 7.0), 0.01) +func InDelta(t TestingT, expected interface{}, actual interface{}, delta float64, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.InDelta(t, expected, actual, delta, msgAndArgs...) { + return + } + t.FailNow() +} + +// InDeltaMapValues is the same as InDelta, but it compares all values between two maps. Both maps must have exactly the same keys. +func InDeltaMapValues(t TestingT, expected interface{}, actual interface{}, delta float64, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.InDeltaMapValues(t, expected, actual, delta, msgAndArgs...) { + return + } + t.FailNow() +} + +// InDeltaMapValuesf is the same as InDelta, but it compares all values between two maps. Both maps must have exactly the same keys. +func InDeltaMapValuesf(t TestingT, expected interface{}, actual interface{}, delta float64, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.InDeltaMapValuesf(t, expected, actual, delta, msg, args...) { + return + } + t.FailNow() +} + +// InDeltaSlice is the same as InDelta, except it compares two slices. +func InDeltaSlice(t TestingT, expected interface{}, actual interface{}, delta float64, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.InDeltaSlice(t, expected, actual, delta, msgAndArgs...) { + return + } + t.FailNow() +} + +// InDeltaSlicef is the same as InDelta, except it compares two slices. +func InDeltaSlicef(t TestingT, expected interface{}, actual interface{}, delta float64, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.InDeltaSlicef(t, expected, actual, delta, msg, args...) { + return + } + t.FailNow() +} + +// InDeltaf asserts that the two numerals are within delta of each other. +// +// assert.InDeltaf(t, math.Pi, (22 / 7.0, "error message %s", "formatted"), 0.01) +func InDeltaf(t TestingT, expected interface{}, actual interface{}, delta float64, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.InDeltaf(t, expected, actual, delta, msg, args...) { + return + } + t.FailNow() +} + +// InEpsilon asserts that expected and actual have a relative error less than epsilon +func InEpsilon(t TestingT, expected interface{}, actual interface{}, epsilon float64, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.InEpsilon(t, expected, actual, epsilon, msgAndArgs...) { + return + } + t.FailNow() +} + +// InEpsilonSlice is the same as InEpsilon, except it compares each value from two slices. +func InEpsilonSlice(t TestingT, expected interface{}, actual interface{}, epsilon float64, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.InEpsilonSlice(t, expected, actual, epsilon, msgAndArgs...) { + return + } + t.FailNow() +} + +// InEpsilonSlicef is the same as InEpsilon, except it compares each value from two slices. +func InEpsilonSlicef(t TestingT, expected interface{}, actual interface{}, epsilon float64, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.InEpsilonSlicef(t, expected, actual, epsilon, msg, args...) { + return + } + t.FailNow() +} + +// InEpsilonf asserts that expected and actual have a relative error less than epsilon +func InEpsilonf(t TestingT, expected interface{}, actual interface{}, epsilon float64, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.InEpsilonf(t, expected, actual, epsilon, msg, args...) { + return + } + t.FailNow() +} + +// IsType asserts that the specified objects are of the same type. +func IsType(t TestingT, expectedType interface{}, object interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.IsType(t, expectedType, object, msgAndArgs...) { + return + } + t.FailNow() +} + +// IsTypef asserts that the specified objects are of the same type. +func IsTypef(t TestingT, expectedType interface{}, object interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.IsTypef(t, expectedType, object, msg, args...) { + return + } + t.FailNow() +} + +// JSONEq asserts that two JSON strings are equivalent. +// +// assert.JSONEq(t, `{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`) +func JSONEq(t TestingT, expected string, actual string, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.JSONEq(t, expected, actual, msgAndArgs...) { + return + } + t.FailNow() +} + +// JSONEqf asserts that two JSON strings are equivalent. +// +// assert.JSONEqf(t, `{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`, "error message %s", "formatted") +func JSONEqf(t TestingT, expected string, actual string, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.JSONEqf(t, expected, actual, msg, args...) { + return + } + t.FailNow() +} + +// YAMLEq asserts that two YAML strings are equivalent. +func YAMLEq(t TestingT, expected string, actual string, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.YAMLEq(t, expected, actual, msgAndArgs...) { + return + } + t.FailNow() +} + +// YAMLEqf asserts that two YAML strings are equivalent. +func YAMLEqf(t TestingT, expected string, actual string, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.YAMLEqf(t, expected, actual, msg, args...) { + return + } + t.FailNow() +} + +// Len asserts that the specified object has specific length. +// Len also fails if the object has a type that len() not accept. +// +// assert.Len(t, mySlice, 3) +func Len(t TestingT, object interface{}, length int, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Len(t, object, length, msgAndArgs...) { + return + } + t.FailNow() +} + +// Lenf asserts that the specified object has specific length. +// Lenf also fails if the object has a type that len() not accept. +// +// assert.Lenf(t, mySlice, 3, "error message %s", "formatted") +func Lenf(t TestingT, object interface{}, length int, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Lenf(t, object, length, msg, args...) { + return + } + t.FailNow() +} + +// Less asserts that the first element is less than the second +// +// assert.Less(t, 1, 2) +// assert.Less(t, float64(1), float64(2)) +// assert.Less(t, "a", "b") +func Less(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Less(t, e1, e2, msgAndArgs...) { + return + } + t.FailNow() +} + +// LessOrEqual asserts that the first element is less than or equal to the second +// +// assert.LessOrEqual(t, 1, 2) +// assert.LessOrEqual(t, 2, 2) +// assert.LessOrEqual(t, "a", "b") +// assert.LessOrEqual(t, "b", "b") +func LessOrEqual(t TestingT, e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.LessOrEqual(t, e1, e2, msgAndArgs...) { + return + } + t.FailNow() +} + +// LessOrEqualf asserts that the first element is less than or equal to the second +// +// assert.LessOrEqualf(t, 1, 2, "error message %s", "formatted") +// assert.LessOrEqualf(t, 2, 2, "error message %s", "formatted") +// assert.LessOrEqualf(t, "a", "b", "error message %s", "formatted") +// assert.LessOrEqualf(t, "b", "b", "error message %s", "formatted") +func LessOrEqualf(t TestingT, e1 interface{}, e2 interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.LessOrEqualf(t, e1, e2, msg, args...) { + return + } + t.FailNow() +} + +// Lessf asserts that the first element is less than the second +// +// assert.Lessf(t, 1, 2, "error message %s", "formatted") +// assert.Lessf(t, float64(1, "error message %s", "formatted"), float64(2)) +// assert.Lessf(t, "a", "b", "error message %s", "formatted") +func Lessf(t TestingT, e1 interface{}, e2 interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Lessf(t, e1, e2, msg, args...) { + return + } + t.FailNow() +} + +// Nil asserts that the specified object is nil. +// +// assert.Nil(t, err) +func Nil(t TestingT, object interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Nil(t, object, msgAndArgs...) { + return + } + t.FailNow() +} + +// Nilf asserts that the specified object is nil. +// +// assert.Nilf(t, err, "error message %s", "formatted") +func Nilf(t TestingT, object interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Nilf(t, object, msg, args...) { + return + } + t.FailNow() +} + +// NoError asserts that a function returned no error (i.e. `nil`). +// +// actualObj, err := SomeFunction() +// if assert.NoError(t, err) { +// assert.Equal(t, expectedObj, actualObj) +// } +func NoError(t TestingT, err error, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NoError(t, err, msgAndArgs...) { + return + } + t.FailNow() +} + +// NoErrorf asserts that a function returned no error (i.e. `nil`). +// +// actualObj, err := SomeFunction() +// if assert.NoErrorf(t, err, "error message %s", "formatted") { +// assert.Equal(t, expectedObj, actualObj) +// } +func NoErrorf(t TestingT, err error, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NoErrorf(t, err, msg, args...) { + return + } + t.FailNow() +} + +// NotContains asserts that the specified string, list(array, slice...) or map does NOT contain the +// specified substring or element. +// +// assert.NotContains(t, "Hello World", "Earth") +// assert.NotContains(t, ["Hello", "World"], "Earth") +// assert.NotContains(t, {"Hello": "World"}, "Earth") +func NotContains(t TestingT, s interface{}, contains interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotContains(t, s, contains, msgAndArgs...) { + return + } + t.FailNow() +} + +// NotContainsf asserts that the specified string, list(array, slice...) or map does NOT contain the +// specified substring or element. +// +// assert.NotContainsf(t, "Hello World", "Earth", "error message %s", "formatted") +// assert.NotContainsf(t, ["Hello", "World"], "Earth", "error message %s", "formatted") +// assert.NotContainsf(t, {"Hello": "World"}, "Earth", "error message %s", "formatted") +func NotContainsf(t TestingT, s interface{}, contains interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotContainsf(t, s, contains, msg, args...) { + return + } + t.FailNow() +} + +// NotEmpty asserts that the specified object is NOT empty. I.e. not nil, "", false, 0 or either +// a slice or a channel with len == 0. +// +// if assert.NotEmpty(t, obj) { +// assert.Equal(t, "two", obj[1]) +// } +func NotEmpty(t TestingT, object interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotEmpty(t, object, msgAndArgs...) { + return + } + t.FailNow() +} + +// NotEmptyf asserts that the specified object is NOT empty. I.e. not nil, "", false, 0 or either +// a slice or a channel with len == 0. +// +// if assert.NotEmptyf(t, obj, "error message %s", "formatted") { +// assert.Equal(t, "two", obj[1]) +// } +func NotEmptyf(t TestingT, object interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotEmptyf(t, object, msg, args...) { + return + } + t.FailNow() +} + +// NotEqual asserts that the specified values are NOT equal. +// +// assert.NotEqual(t, obj1, obj2) +// +// Pointer variable equality is determined based on the equality of the +// referenced values (as opposed to the memory addresses). +func NotEqual(t TestingT, expected interface{}, actual interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotEqual(t, expected, actual, msgAndArgs...) { + return + } + t.FailNow() +} + +// NotEqualf asserts that the specified values are NOT equal. +// +// assert.NotEqualf(t, obj1, obj2, "error message %s", "formatted") +// +// Pointer variable equality is determined based on the equality of the +// referenced values (as opposed to the memory addresses). +func NotEqualf(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotEqualf(t, expected, actual, msg, args...) { + return + } + t.FailNow() +} + +// NotNil asserts that the specified object is not nil. +// +// assert.NotNil(t, err) +func NotNil(t TestingT, object interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotNil(t, object, msgAndArgs...) { + return + } + t.FailNow() +} + +// NotNilf asserts that the specified object is not nil. +// +// assert.NotNilf(t, err, "error message %s", "formatted") +func NotNilf(t TestingT, object interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotNilf(t, object, msg, args...) { + return + } + t.FailNow() +} + +// NotPanics asserts that the code inside the specified PanicTestFunc does NOT panic. +// +// assert.NotPanics(t, func(){ RemainCalm() }) +func NotPanics(t TestingT, f assert.PanicTestFunc, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotPanics(t, f, msgAndArgs...) { + return + } + t.FailNow() +} + +// NotPanicsf asserts that the code inside the specified PanicTestFunc does NOT panic. +// +// assert.NotPanicsf(t, func(){ RemainCalm() }, "error message %s", "formatted") +func NotPanicsf(t TestingT, f assert.PanicTestFunc, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotPanicsf(t, f, msg, args...) { + return + } + t.FailNow() +} + +// NotRegexp asserts that a specified regexp does not match a string. +// +// assert.NotRegexp(t, regexp.MustCompile("starts"), "it's starting") +// assert.NotRegexp(t, "^start", "it's not starting") +func NotRegexp(t TestingT, rx interface{}, str interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotRegexp(t, rx, str, msgAndArgs...) { + return + } + t.FailNow() +} + +// NotRegexpf asserts that a specified regexp does not match a string. +// +// assert.NotRegexpf(t, regexp.MustCompile("starts", "error message %s", "formatted"), "it's starting") +// assert.NotRegexpf(t, "^start", "it's not starting", "error message %s", "formatted") +func NotRegexpf(t TestingT, rx interface{}, str interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotRegexpf(t, rx, str, msg, args...) { + return + } + t.FailNow() +} + +// NotSubset asserts that the specified list(array, slice...) contains not all +// elements given in the specified subset(array, slice...). +// +// assert.NotSubset(t, [1, 3, 4], [1, 2], "But [1, 3, 4] does not contain [1, 2]") +func NotSubset(t TestingT, list interface{}, subset interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotSubset(t, list, subset, msgAndArgs...) { + return + } + t.FailNow() +} + +// NotSubsetf asserts that the specified list(array, slice...) contains not all +// elements given in the specified subset(array, slice...). +// +// assert.NotSubsetf(t, [1, 3, 4], [1, 2], "But [1, 3, 4] does not contain [1, 2]", "error message %s", "formatted") +func NotSubsetf(t TestingT, list interface{}, subset interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotSubsetf(t, list, subset, msg, args...) { + return + } + t.FailNow() +} + +// NotZero asserts that i is not the zero value for its type. +func NotZero(t TestingT, i interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotZero(t, i, msgAndArgs...) { + return + } + t.FailNow() +} + +// NotZerof asserts that i is not the zero value for its type. +func NotZerof(t TestingT, i interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.NotZerof(t, i, msg, args...) { + return + } + t.FailNow() +} + +// Panics asserts that the code inside the specified PanicTestFunc panics. +// +// assert.Panics(t, func(){ GoCrazy() }) +func Panics(t TestingT, f assert.PanicTestFunc, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Panics(t, f, msgAndArgs...) { + return + } + t.FailNow() +} + +// PanicsWithValue asserts that the code inside the specified PanicTestFunc panics, and that +// the recovered panic value equals the expected panic value. +// +// assert.PanicsWithValue(t, "crazy error", func(){ GoCrazy() }) +func PanicsWithValue(t TestingT, expected interface{}, f assert.PanicTestFunc, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.PanicsWithValue(t, expected, f, msgAndArgs...) { + return + } + t.FailNow() +} + +// PanicsWithValuef asserts that the code inside the specified PanicTestFunc panics, and that +// the recovered panic value equals the expected panic value. +// +// assert.PanicsWithValuef(t, "crazy error", func(){ GoCrazy() }, "error message %s", "formatted") +func PanicsWithValuef(t TestingT, expected interface{}, f assert.PanicTestFunc, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.PanicsWithValuef(t, expected, f, msg, args...) { + return + } + t.FailNow() +} + +// Panicsf asserts that the code inside the specified PanicTestFunc panics. +// +// assert.Panicsf(t, func(){ GoCrazy() }, "error message %s", "formatted") +func Panicsf(t TestingT, f assert.PanicTestFunc, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Panicsf(t, f, msg, args...) { + return + } + t.FailNow() +} + +// Regexp asserts that a specified regexp matches a string. +// +// assert.Regexp(t, regexp.MustCompile("start"), "it's starting") +// assert.Regexp(t, "start...$", "it's not starting") +func Regexp(t TestingT, rx interface{}, str interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Regexp(t, rx, str, msgAndArgs...) { + return + } + t.FailNow() +} + +// Regexpf asserts that a specified regexp matches a string. +// +// assert.Regexpf(t, regexp.MustCompile("start", "error message %s", "formatted"), "it's starting") +// assert.Regexpf(t, "start...$", "it's not starting", "error message %s", "formatted") +func Regexpf(t TestingT, rx interface{}, str interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Regexpf(t, rx, str, msg, args...) { + return + } + t.FailNow() +} + +// Same asserts that two pointers reference the same object. +// +// assert.Same(t, ptr1, ptr2) +// +// Both arguments must be pointer variables. Pointer variable sameness is +// determined based on the equality of both type and value. +func Same(t TestingT, expected interface{}, actual interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Same(t, expected, actual, msgAndArgs...) { + return + } + t.FailNow() +} + +// Samef asserts that two pointers reference the same object. +// +// assert.Samef(t, ptr1, ptr2, "error message %s", "formatted") +// +// Both arguments must be pointer variables. Pointer variable sameness is +// determined based on the equality of both type and value. +func Samef(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Samef(t, expected, actual, msg, args...) { + return + } + t.FailNow() +} + +// Subset asserts that the specified list(array, slice...) contains all +// elements given in the specified subset(array, slice...). +// +// assert.Subset(t, [1, 2, 3], [1, 2], "But [1, 2, 3] does contain [1, 2]") +func Subset(t TestingT, list interface{}, subset interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Subset(t, list, subset, msgAndArgs...) { + return + } + t.FailNow() +} + +// Subsetf asserts that the specified list(array, slice...) contains all +// elements given in the specified subset(array, slice...). +// +// assert.Subsetf(t, [1, 2, 3], [1, 2], "But [1, 2, 3] does contain [1, 2]", "error message %s", "formatted") +func Subsetf(t TestingT, list interface{}, subset interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Subsetf(t, list, subset, msg, args...) { + return + } + t.FailNow() +} + +// True asserts that the specified value is true. +// +// assert.True(t, myBool) +func True(t TestingT, value bool, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.True(t, value, msgAndArgs...) { + return + } + t.FailNow() +} + +// Truef asserts that the specified value is true. +// +// assert.Truef(t, myBool, "error message %s", "formatted") +func Truef(t TestingT, value bool, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Truef(t, value, msg, args...) { + return + } + t.FailNow() +} + +// WithinDuration asserts that the two times are within duration delta of each other. +// +// assert.WithinDuration(t, time.Now(), time.Now(), 10*time.Second) +func WithinDuration(t TestingT, expected time.Time, actual time.Time, delta time.Duration, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.WithinDuration(t, expected, actual, delta, msgAndArgs...) { + return + } + t.FailNow() +} + +// WithinDurationf asserts that the two times are within duration delta of each other. +// +// assert.WithinDurationf(t, time.Now(), time.Now(), 10*time.Second, "error message %s", "formatted") +func WithinDurationf(t TestingT, expected time.Time, actual time.Time, delta time.Duration, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.WithinDurationf(t, expected, actual, delta, msg, args...) { + return + } + t.FailNow() +} + +// Zero asserts that i is the zero value for its type. +func Zero(t TestingT, i interface{}, msgAndArgs ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Zero(t, i, msgAndArgs...) { + return + } + t.FailNow() +} + +// Zerof asserts that i is the zero value for its type. +func Zerof(t TestingT, i interface{}, msg string, args ...interface{}) { + if h, ok := t.(tHelper); ok { + h.Helper() + } + if assert.Zerof(t, i, msg, args...) { + return + } + t.FailNow() +} diff --git a/vendor/github.com/stretchr/testify/require/require_forward.go b/vendor/github.com/stretchr/testify/require/require_forward.go new file mode 100644 index 000000000..804fae035 --- /dev/null +++ b/vendor/github.com/stretchr/testify/require/require_forward.go @@ -0,0 +1,1121 @@ +/* +* CODE GENERATED AUTOMATICALLY WITH github.com/stretchr/testify/_codegen +* THIS FILE MUST NOT BE EDITED BY HAND + */ + +package require + +import ( + assert "github.com/stretchr/testify/assert" + http "net/http" + url "net/url" + time "time" +) + +// Condition uses a Comparison to assert a complex condition. +func (a *Assertions) Condition(comp assert.Comparison, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Condition(a.t, comp, msgAndArgs...) +} + +// Conditionf uses a Comparison to assert a complex condition. +func (a *Assertions) Conditionf(comp assert.Comparison, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Conditionf(a.t, comp, msg, args...) +} + +// Contains asserts that the specified string, list(array, slice...) or map contains the +// specified substring or element. +// +// a.Contains("Hello World", "World") +// a.Contains(["Hello", "World"], "World") +// a.Contains({"Hello": "World"}, "Hello") +func (a *Assertions) Contains(s interface{}, contains interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Contains(a.t, s, contains, msgAndArgs...) +} + +// Containsf asserts that the specified string, list(array, slice...) or map contains the +// specified substring or element. +// +// a.Containsf("Hello World", "World", "error message %s", "formatted") +// a.Containsf(["Hello", "World"], "World", "error message %s", "formatted") +// a.Containsf({"Hello": "World"}, "Hello", "error message %s", "formatted") +func (a *Assertions) Containsf(s interface{}, contains interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Containsf(a.t, s, contains, msg, args...) +} + +// DirExists checks whether a directory exists in the given path. It also fails if the path is a file rather a directory or there is an error checking whether it exists. +func (a *Assertions) DirExists(path string, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + DirExists(a.t, path, msgAndArgs...) +} + +// DirExistsf checks whether a directory exists in the given path. It also fails if the path is a file rather a directory or there is an error checking whether it exists. +func (a *Assertions) DirExistsf(path string, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + DirExistsf(a.t, path, msg, args...) +} + +// ElementsMatch asserts that the specified listA(array, slice...) is equal to specified +// listB(array, slice...) ignoring the order of the elements. If there are duplicate elements, +// the number of appearances of each of them in both lists should match. +// +// a.ElementsMatch([1, 3, 2, 3], [1, 3, 3, 2]) +func (a *Assertions) ElementsMatch(listA interface{}, listB interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + ElementsMatch(a.t, listA, listB, msgAndArgs...) +} + +// ElementsMatchf asserts that the specified listA(array, slice...) is equal to specified +// listB(array, slice...) ignoring the order of the elements. If there are duplicate elements, +// the number of appearances of each of them in both lists should match. +// +// a.ElementsMatchf([1, 3, 2, 3], [1, 3, 3, 2], "error message %s", "formatted") +func (a *Assertions) ElementsMatchf(listA interface{}, listB interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + ElementsMatchf(a.t, listA, listB, msg, args...) +} + +// Empty asserts that the specified object is empty. I.e. nil, "", false, 0 or either +// a slice or a channel with len == 0. +// +// a.Empty(obj) +func (a *Assertions) Empty(object interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Empty(a.t, object, msgAndArgs...) +} + +// Emptyf asserts that the specified object is empty. I.e. nil, "", false, 0 or either +// a slice or a channel with len == 0. +// +// a.Emptyf(obj, "error message %s", "formatted") +func (a *Assertions) Emptyf(object interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Emptyf(a.t, object, msg, args...) +} + +// Equal asserts that two objects are equal. +// +// a.Equal(123, 123) +// +// Pointer variable equality is determined based on the equality of the +// referenced values (as opposed to the memory addresses). Function equality +// cannot be determined and will always fail. +func (a *Assertions) Equal(expected interface{}, actual interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Equal(a.t, expected, actual, msgAndArgs...) +} + +// EqualError asserts that a function returned an error (i.e. not `nil`) +// and that it is equal to the provided error. +// +// actualObj, err := SomeFunction() +// a.EqualError(err, expectedErrorString) +func (a *Assertions) EqualError(theError error, errString string, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + EqualError(a.t, theError, errString, msgAndArgs...) +} + +// EqualErrorf asserts that a function returned an error (i.e. not `nil`) +// and that it is equal to the provided error. +// +// actualObj, err := SomeFunction() +// a.EqualErrorf(err, expectedErrorString, "error message %s", "formatted") +func (a *Assertions) EqualErrorf(theError error, errString string, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + EqualErrorf(a.t, theError, errString, msg, args...) +} + +// EqualValues asserts that two objects are equal or convertable to the same types +// and equal. +// +// a.EqualValues(uint32(123), int32(123)) +func (a *Assertions) EqualValues(expected interface{}, actual interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + EqualValues(a.t, expected, actual, msgAndArgs...) +} + +// EqualValuesf asserts that two objects are equal or convertable to the same types +// and equal. +// +// a.EqualValuesf(uint32(123, "error message %s", "formatted"), int32(123)) +func (a *Assertions) EqualValuesf(expected interface{}, actual interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + EqualValuesf(a.t, expected, actual, msg, args...) +} + +// Equalf asserts that two objects are equal. +// +// a.Equalf(123, 123, "error message %s", "formatted") +// +// Pointer variable equality is determined based on the equality of the +// referenced values (as opposed to the memory addresses). Function equality +// cannot be determined and will always fail. +func (a *Assertions) Equalf(expected interface{}, actual interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Equalf(a.t, expected, actual, msg, args...) +} + +// Error asserts that a function returned an error (i.e. not `nil`). +// +// actualObj, err := SomeFunction() +// if a.Error(err) { +// assert.Equal(t, expectedError, err) +// } +func (a *Assertions) Error(err error, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Error(a.t, err, msgAndArgs...) +} + +// Errorf asserts that a function returned an error (i.e. not `nil`). +// +// actualObj, err := SomeFunction() +// if a.Errorf(err, "error message %s", "formatted") { +// assert.Equal(t, expectedErrorf, err) +// } +func (a *Assertions) Errorf(err error, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Errorf(a.t, err, msg, args...) +} + +// Eventually asserts that given condition will be met in waitFor time, +// periodically checking target function each tick. +// +// a.Eventually(func() bool { return true; }, time.Second, 10*time.Millisecond) +func (a *Assertions) Eventually(condition func() bool, waitFor time.Duration, tick time.Duration, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Eventually(a.t, condition, waitFor, tick, msgAndArgs...) +} + +// Eventuallyf asserts that given condition will be met in waitFor time, +// periodically checking target function each tick. +// +// a.Eventuallyf(func() bool { return true; }, time.Second, 10*time.Millisecond, "error message %s", "formatted") +func (a *Assertions) Eventuallyf(condition func() bool, waitFor time.Duration, tick time.Duration, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Eventuallyf(a.t, condition, waitFor, tick, msg, args...) +} + +// Exactly asserts that two objects are equal in value and type. +// +// a.Exactly(int32(123), int64(123)) +func (a *Assertions) Exactly(expected interface{}, actual interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Exactly(a.t, expected, actual, msgAndArgs...) +} + +// Exactlyf asserts that two objects are equal in value and type. +// +// a.Exactlyf(int32(123, "error message %s", "formatted"), int64(123)) +func (a *Assertions) Exactlyf(expected interface{}, actual interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Exactlyf(a.t, expected, actual, msg, args...) +} + +// Fail reports a failure through +func (a *Assertions) Fail(failureMessage string, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Fail(a.t, failureMessage, msgAndArgs...) +} + +// FailNow fails test +func (a *Assertions) FailNow(failureMessage string, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + FailNow(a.t, failureMessage, msgAndArgs...) +} + +// FailNowf fails test +func (a *Assertions) FailNowf(failureMessage string, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + FailNowf(a.t, failureMessage, msg, args...) +} + +// Failf reports a failure through +func (a *Assertions) Failf(failureMessage string, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Failf(a.t, failureMessage, msg, args...) +} + +// False asserts that the specified value is false. +// +// a.False(myBool) +func (a *Assertions) False(value bool, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + False(a.t, value, msgAndArgs...) +} + +// Falsef asserts that the specified value is false. +// +// a.Falsef(myBool, "error message %s", "formatted") +func (a *Assertions) Falsef(value bool, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Falsef(a.t, value, msg, args...) +} + +// FileExists checks whether a file exists in the given path. It also fails if the path points to a directory or there is an error when trying to check the file. +func (a *Assertions) FileExists(path string, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + FileExists(a.t, path, msgAndArgs...) +} + +// FileExistsf checks whether a file exists in the given path. It also fails if the path points to a directory or there is an error when trying to check the file. +func (a *Assertions) FileExistsf(path string, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + FileExistsf(a.t, path, msg, args...) +} + +// Greater asserts that the first element is greater than the second +// +// a.Greater(2, 1) +// a.Greater(float64(2), float64(1)) +// a.Greater("b", "a") +func (a *Assertions) Greater(e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Greater(a.t, e1, e2, msgAndArgs...) +} + +// GreaterOrEqual asserts that the first element is greater than or equal to the second +// +// a.GreaterOrEqual(2, 1) +// a.GreaterOrEqual(2, 2) +// a.GreaterOrEqual("b", "a") +// a.GreaterOrEqual("b", "b") +func (a *Assertions) GreaterOrEqual(e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + GreaterOrEqual(a.t, e1, e2, msgAndArgs...) +} + +// GreaterOrEqualf asserts that the first element is greater than or equal to the second +// +// a.GreaterOrEqualf(2, 1, "error message %s", "formatted") +// a.GreaterOrEqualf(2, 2, "error message %s", "formatted") +// a.GreaterOrEqualf("b", "a", "error message %s", "formatted") +// a.GreaterOrEqualf("b", "b", "error message %s", "formatted") +func (a *Assertions) GreaterOrEqualf(e1 interface{}, e2 interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + GreaterOrEqualf(a.t, e1, e2, msg, args...) +} + +// Greaterf asserts that the first element is greater than the second +// +// a.Greaterf(2, 1, "error message %s", "formatted") +// a.Greaterf(float64(2, "error message %s", "formatted"), float64(1)) +// a.Greaterf("b", "a", "error message %s", "formatted") +func (a *Assertions) Greaterf(e1 interface{}, e2 interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Greaterf(a.t, e1, e2, msg, args...) +} + +// HTTPBodyContains asserts that a specified handler returns a +// body that contains a string. +// +// a.HTTPBodyContains(myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPBodyContains(handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + HTTPBodyContains(a.t, handler, method, url, values, str, msgAndArgs...) +} + +// HTTPBodyContainsf asserts that a specified handler returns a +// body that contains a string. +// +// a.HTTPBodyContainsf(myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky", "error message %s", "formatted") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPBodyContainsf(handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + HTTPBodyContainsf(a.t, handler, method, url, values, str, msg, args...) +} + +// HTTPBodyNotContains asserts that a specified handler returns a +// body that does not contain a string. +// +// a.HTTPBodyNotContains(myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPBodyNotContains(handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + HTTPBodyNotContains(a.t, handler, method, url, values, str, msgAndArgs...) +} + +// HTTPBodyNotContainsf asserts that a specified handler returns a +// body that does not contain a string. +// +// a.HTTPBodyNotContainsf(myHandler, "GET", "www.google.com", nil, "I'm Feeling Lucky", "error message %s", "formatted") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPBodyNotContainsf(handler http.HandlerFunc, method string, url string, values url.Values, str interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + HTTPBodyNotContainsf(a.t, handler, method, url, values, str, msg, args...) +} + +// HTTPError asserts that a specified handler returns an error status code. +// +// a.HTTPError(myHandler, "POST", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPError(handler http.HandlerFunc, method string, url string, values url.Values, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + HTTPError(a.t, handler, method, url, values, msgAndArgs...) +} + +// HTTPErrorf asserts that a specified handler returns an error status code. +// +// a.HTTPErrorf(myHandler, "POST", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// +// Returns whether the assertion was successful (true, "error message %s", "formatted") or not (false). +func (a *Assertions) HTTPErrorf(handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + HTTPErrorf(a.t, handler, method, url, values, msg, args...) +} + +// HTTPRedirect asserts that a specified handler returns a redirect status code. +// +// a.HTTPRedirect(myHandler, "GET", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPRedirect(handler http.HandlerFunc, method string, url string, values url.Values, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + HTTPRedirect(a.t, handler, method, url, values, msgAndArgs...) +} + +// HTTPRedirectf asserts that a specified handler returns a redirect status code. +// +// a.HTTPRedirectf(myHandler, "GET", "/a/b/c", url.Values{"a": []string{"b", "c"}} +// +// Returns whether the assertion was successful (true, "error message %s", "formatted") or not (false). +func (a *Assertions) HTTPRedirectf(handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + HTTPRedirectf(a.t, handler, method, url, values, msg, args...) +} + +// HTTPSuccess asserts that a specified handler returns a success status code. +// +// a.HTTPSuccess(myHandler, "POST", "http://www.google.com", nil) +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPSuccess(handler http.HandlerFunc, method string, url string, values url.Values, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + HTTPSuccess(a.t, handler, method, url, values, msgAndArgs...) +} + +// HTTPSuccessf asserts that a specified handler returns a success status code. +// +// a.HTTPSuccessf(myHandler, "POST", "http://www.google.com", nil, "error message %s", "formatted") +// +// Returns whether the assertion was successful (true) or not (false). +func (a *Assertions) HTTPSuccessf(handler http.HandlerFunc, method string, url string, values url.Values, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + HTTPSuccessf(a.t, handler, method, url, values, msg, args...) +} + +// Implements asserts that an object is implemented by the specified interface. +// +// a.Implements((*MyInterface)(nil), new(MyObject)) +func (a *Assertions) Implements(interfaceObject interface{}, object interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Implements(a.t, interfaceObject, object, msgAndArgs...) +} + +// Implementsf asserts that an object is implemented by the specified interface. +// +// a.Implementsf((*MyInterface, "error message %s", "formatted")(nil), new(MyObject)) +func (a *Assertions) Implementsf(interfaceObject interface{}, object interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Implementsf(a.t, interfaceObject, object, msg, args...) +} + +// InDelta asserts that the two numerals are within delta of each other. +// +// a.InDelta(math.Pi, (22 / 7.0), 0.01) +func (a *Assertions) InDelta(expected interface{}, actual interface{}, delta float64, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + InDelta(a.t, expected, actual, delta, msgAndArgs...) +} + +// InDeltaMapValues is the same as InDelta, but it compares all values between two maps. Both maps must have exactly the same keys. +func (a *Assertions) InDeltaMapValues(expected interface{}, actual interface{}, delta float64, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + InDeltaMapValues(a.t, expected, actual, delta, msgAndArgs...) +} + +// InDeltaMapValuesf is the same as InDelta, but it compares all values between two maps. Both maps must have exactly the same keys. +func (a *Assertions) InDeltaMapValuesf(expected interface{}, actual interface{}, delta float64, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + InDeltaMapValuesf(a.t, expected, actual, delta, msg, args...) +} + +// InDeltaSlice is the same as InDelta, except it compares two slices. +func (a *Assertions) InDeltaSlice(expected interface{}, actual interface{}, delta float64, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + InDeltaSlice(a.t, expected, actual, delta, msgAndArgs...) +} + +// InDeltaSlicef is the same as InDelta, except it compares two slices. +func (a *Assertions) InDeltaSlicef(expected interface{}, actual interface{}, delta float64, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + InDeltaSlicef(a.t, expected, actual, delta, msg, args...) +} + +// InDeltaf asserts that the two numerals are within delta of each other. +// +// a.InDeltaf(math.Pi, (22 / 7.0, "error message %s", "formatted"), 0.01) +func (a *Assertions) InDeltaf(expected interface{}, actual interface{}, delta float64, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + InDeltaf(a.t, expected, actual, delta, msg, args...) +} + +// InEpsilon asserts that expected and actual have a relative error less than epsilon +func (a *Assertions) InEpsilon(expected interface{}, actual interface{}, epsilon float64, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + InEpsilon(a.t, expected, actual, epsilon, msgAndArgs...) +} + +// InEpsilonSlice is the same as InEpsilon, except it compares each value from two slices. +func (a *Assertions) InEpsilonSlice(expected interface{}, actual interface{}, epsilon float64, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + InEpsilonSlice(a.t, expected, actual, epsilon, msgAndArgs...) +} + +// InEpsilonSlicef is the same as InEpsilon, except it compares each value from two slices. +func (a *Assertions) InEpsilonSlicef(expected interface{}, actual interface{}, epsilon float64, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + InEpsilonSlicef(a.t, expected, actual, epsilon, msg, args...) +} + +// InEpsilonf asserts that expected and actual have a relative error less than epsilon +func (a *Assertions) InEpsilonf(expected interface{}, actual interface{}, epsilon float64, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + InEpsilonf(a.t, expected, actual, epsilon, msg, args...) +} + +// IsType asserts that the specified objects are of the same type. +func (a *Assertions) IsType(expectedType interface{}, object interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + IsType(a.t, expectedType, object, msgAndArgs...) +} + +// IsTypef asserts that the specified objects are of the same type. +func (a *Assertions) IsTypef(expectedType interface{}, object interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + IsTypef(a.t, expectedType, object, msg, args...) +} + +// JSONEq asserts that two JSON strings are equivalent. +// +// a.JSONEq(`{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`) +func (a *Assertions) JSONEq(expected string, actual string, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + JSONEq(a.t, expected, actual, msgAndArgs...) +} + +// JSONEqf asserts that two JSON strings are equivalent. +// +// a.JSONEqf(`{"hello": "world", "foo": "bar"}`, `{"foo": "bar", "hello": "world"}`, "error message %s", "formatted") +func (a *Assertions) JSONEqf(expected string, actual string, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + JSONEqf(a.t, expected, actual, msg, args...) +} + +// YAMLEq asserts that two YAML strings are equivalent. +func (a *Assertions) YAMLEq(expected string, actual string, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + YAMLEq(a.t, expected, actual, msgAndArgs...) +} + +// YAMLEqf asserts that two YAML strings are equivalent. +func (a *Assertions) YAMLEqf(expected string, actual string, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + YAMLEqf(a.t, expected, actual, msg, args...) +} + +// Len asserts that the specified object has specific length. +// Len also fails if the object has a type that len() not accept. +// +// a.Len(mySlice, 3) +func (a *Assertions) Len(object interface{}, length int, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Len(a.t, object, length, msgAndArgs...) +} + +// Lenf asserts that the specified object has specific length. +// Lenf also fails if the object has a type that len() not accept. +// +// a.Lenf(mySlice, 3, "error message %s", "formatted") +func (a *Assertions) Lenf(object interface{}, length int, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Lenf(a.t, object, length, msg, args...) +} + +// Less asserts that the first element is less than the second +// +// a.Less(1, 2) +// a.Less(float64(1), float64(2)) +// a.Less("a", "b") +func (a *Assertions) Less(e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Less(a.t, e1, e2, msgAndArgs...) +} + +// LessOrEqual asserts that the first element is less than or equal to the second +// +// a.LessOrEqual(1, 2) +// a.LessOrEqual(2, 2) +// a.LessOrEqual("a", "b") +// a.LessOrEqual("b", "b") +func (a *Assertions) LessOrEqual(e1 interface{}, e2 interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + LessOrEqual(a.t, e1, e2, msgAndArgs...) +} + +// LessOrEqualf asserts that the first element is less than or equal to the second +// +// a.LessOrEqualf(1, 2, "error message %s", "formatted") +// a.LessOrEqualf(2, 2, "error message %s", "formatted") +// a.LessOrEqualf("a", "b", "error message %s", "formatted") +// a.LessOrEqualf("b", "b", "error message %s", "formatted") +func (a *Assertions) LessOrEqualf(e1 interface{}, e2 interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + LessOrEqualf(a.t, e1, e2, msg, args...) +} + +// Lessf asserts that the first element is less than the second +// +// a.Lessf(1, 2, "error message %s", "formatted") +// a.Lessf(float64(1, "error message %s", "formatted"), float64(2)) +// a.Lessf("a", "b", "error message %s", "formatted") +func (a *Assertions) Lessf(e1 interface{}, e2 interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Lessf(a.t, e1, e2, msg, args...) +} + +// Nil asserts that the specified object is nil. +// +// a.Nil(err) +func (a *Assertions) Nil(object interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Nil(a.t, object, msgAndArgs...) +} + +// Nilf asserts that the specified object is nil. +// +// a.Nilf(err, "error message %s", "formatted") +func (a *Assertions) Nilf(object interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Nilf(a.t, object, msg, args...) +} + +// NoError asserts that a function returned no error (i.e. `nil`). +// +// actualObj, err := SomeFunction() +// if a.NoError(err) { +// assert.Equal(t, expectedObj, actualObj) +// } +func (a *Assertions) NoError(err error, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NoError(a.t, err, msgAndArgs...) +} + +// NoErrorf asserts that a function returned no error (i.e. `nil`). +// +// actualObj, err := SomeFunction() +// if a.NoErrorf(err, "error message %s", "formatted") { +// assert.Equal(t, expectedObj, actualObj) +// } +func (a *Assertions) NoErrorf(err error, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NoErrorf(a.t, err, msg, args...) +} + +// NotContains asserts that the specified string, list(array, slice...) or map does NOT contain the +// specified substring or element. +// +// a.NotContains("Hello World", "Earth") +// a.NotContains(["Hello", "World"], "Earth") +// a.NotContains({"Hello": "World"}, "Earth") +func (a *Assertions) NotContains(s interface{}, contains interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotContains(a.t, s, contains, msgAndArgs...) +} + +// NotContainsf asserts that the specified string, list(array, slice...) or map does NOT contain the +// specified substring or element. +// +// a.NotContainsf("Hello World", "Earth", "error message %s", "formatted") +// a.NotContainsf(["Hello", "World"], "Earth", "error message %s", "formatted") +// a.NotContainsf({"Hello": "World"}, "Earth", "error message %s", "formatted") +func (a *Assertions) NotContainsf(s interface{}, contains interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotContainsf(a.t, s, contains, msg, args...) +} + +// NotEmpty asserts that the specified object is NOT empty. I.e. not nil, "", false, 0 or either +// a slice or a channel with len == 0. +// +// if a.NotEmpty(obj) { +// assert.Equal(t, "two", obj[1]) +// } +func (a *Assertions) NotEmpty(object interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotEmpty(a.t, object, msgAndArgs...) +} + +// NotEmptyf asserts that the specified object is NOT empty. I.e. not nil, "", false, 0 or either +// a slice or a channel with len == 0. +// +// if a.NotEmptyf(obj, "error message %s", "formatted") { +// assert.Equal(t, "two", obj[1]) +// } +func (a *Assertions) NotEmptyf(object interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotEmptyf(a.t, object, msg, args...) +} + +// NotEqual asserts that the specified values are NOT equal. +// +// a.NotEqual(obj1, obj2) +// +// Pointer variable equality is determined based on the equality of the +// referenced values (as opposed to the memory addresses). +func (a *Assertions) NotEqual(expected interface{}, actual interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotEqual(a.t, expected, actual, msgAndArgs...) +} + +// NotEqualf asserts that the specified values are NOT equal. +// +// a.NotEqualf(obj1, obj2, "error message %s", "formatted") +// +// Pointer variable equality is determined based on the equality of the +// referenced values (as opposed to the memory addresses). +func (a *Assertions) NotEqualf(expected interface{}, actual interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotEqualf(a.t, expected, actual, msg, args...) +} + +// NotNil asserts that the specified object is not nil. +// +// a.NotNil(err) +func (a *Assertions) NotNil(object interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotNil(a.t, object, msgAndArgs...) +} + +// NotNilf asserts that the specified object is not nil. +// +// a.NotNilf(err, "error message %s", "formatted") +func (a *Assertions) NotNilf(object interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotNilf(a.t, object, msg, args...) +} + +// NotPanics asserts that the code inside the specified PanicTestFunc does NOT panic. +// +// a.NotPanics(func(){ RemainCalm() }) +func (a *Assertions) NotPanics(f assert.PanicTestFunc, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotPanics(a.t, f, msgAndArgs...) +} + +// NotPanicsf asserts that the code inside the specified PanicTestFunc does NOT panic. +// +// a.NotPanicsf(func(){ RemainCalm() }, "error message %s", "formatted") +func (a *Assertions) NotPanicsf(f assert.PanicTestFunc, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotPanicsf(a.t, f, msg, args...) +} + +// NotRegexp asserts that a specified regexp does not match a string. +// +// a.NotRegexp(regexp.MustCompile("starts"), "it's starting") +// a.NotRegexp("^start", "it's not starting") +func (a *Assertions) NotRegexp(rx interface{}, str interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotRegexp(a.t, rx, str, msgAndArgs...) +} + +// NotRegexpf asserts that a specified regexp does not match a string. +// +// a.NotRegexpf(regexp.MustCompile("starts", "error message %s", "formatted"), "it's starting") +// a.NotRegexpf("^start", "it's not starting", "error message %s", "formatted") +func (a *Assertions) NotRegexpf(rx interface{}, str interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotRegexpf(a.t, rx, str, msg, args...) +} + +// NotSubset asserts that the specified list(array, slice...) contains not all +// elements given in the specified subset(array, slice...). +// +// a.NotSubset([1, 3, 4], [1, 2], "But [1, 3, 4] does not contain [1, 2]") +func (a *Assertions) NotSubset(list interface{}, subset interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotSubset(a.t, list, subset, msgAndArgs...) +} + +// NotSubsetf asserts that the specified list(array, slice...) contains not all +// elements given in the specified subset(array, slice...). +// +// a.NotSubsetf([1, 3, 4], [1, 2], "But [1, 3, 4] does not contain [1, 2]", "error message %s", "formatted") +func (a *Assertions) NotSubsetf(list interface{}, subset interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotSubsetf(a.t, list, subset, msg, args...) +} + +// NotZero asserts that i is not the zero value for its type. +func (a *Assertions) NotZero(i interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotZero(a.t, i, msgAndArgs...) +} + +// NotZerof asserts that i is not the zero value for its type. +func (a *Assertions) NotZerof(i interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + NotZerof(a.t, i, msg, args...) +} + +// Panics asserts that the code inside the specified PanicTestFunc panics. +// +// a.Panics(func(){ GoCrazy() }) +func (a *Assertions) Panics(f assert.PanicTestFunc, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Panics(a.t, f, msgAndArgs...) +} + +// PanicsWithValue asserts that the code inside the specified PanicTestFunc panics, and that +// the recovered panic value equals the expected panic value. +// +// a.PanicsWithValue("crazy error", func(){ GoCrazy() }) +func (a *Assertions) PanicsWithValue(expected interface{}, f assert.PanicTestFunc, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + PanicsWithValue(a.t, expected, f, msgAndArgs...) +} + +// PanicsWithValuef asserts that the code inside the specified PanicTestFunc panics, and that +// the recovered panic value equals the expected panic value. +// +// a.PanicsWithValuef("crazy error", func(){ GoCrazy() }, "error message %s", "formatted") +func (a *Assertions) PanicsWithValuef(expected interface{}, f assert.PanicTestFunc, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + PanicsWithValuef(a.t, expected, f, msg, args...) +} + +// Panicsf asserts that the code inside the specified PanicTestFunc panics. +// +// a.Panicsf(func(){ GoCrazy() }, "error message %s", "formatted") +func (a *Assertions) Panicsf(f assert.PanicTestFunc, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Panicsf(a.t, f, msg, args...) +} + +// Regexp asserts that a specified regexp matches a string. +// +// a.Regexp(regexp.MustCompile("start"), "it's starting") +// a.Regexp("start...$", "it's not starting") +func (a *Assertions) Regexp(rx interface{}, str interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Regexp(a.t, rx, str, msgAndArgs...) +} + +// Regexpf asserts that a specified regexp matches a string. +// +// a.Regexpf(regexp.MustCompile("start", "error message %s", "formatted"), "it's starting") +// a.Regexpf("start...$", "it's not starting", "error message %s", "formatted") +func (a *Assertions) Regexpf(rx interface{}, str interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Regexpf(a.t, rx, str, msg, args...) +} + +// Same asserts that two pointers reference the same object. +// +// a.Same(ptr1, ptr2) +// +// Both arguments must be pointer variables. Pointer variable sameness is +// determined based on the equality of both type and value. +func (a *Assertions) Same(expected interface{}, actual interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Same(a.t, expected, actual, msgAndArgs...) +} + +// Samef asserts that two pointers reference the same object. +// +// a.Samef(ptr1, ptr2, "error message %s", "formatted") +// +// Both arguments must be pointer variables. Pointer variable sameness is +// determined based on the equality of both type and value. +func (a *Assertions) Samef(expected interface{}, actual interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Samef(a.t, expected, actual, msg, args...) +} + +// Subset asserts that the specified list(array, slice...) contains all +// elements given in the specified subset(array, slice...). +// +// a.Subset([1, 2, 3], [1, 2], "But [1, 2, 3] does contain [1, 2]") +func (a *Assertions) Subset(list interface{}, subset interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Subset(a.t, list, subset, msgAndArgs...) +} + +// Subsetf asserts that the specified list(array, slice...) contains all +// elements given in the specified subset(array, slice...). +// +// a.Subsetf([1, 2, 3], [1, 2], "But [1, 2, 3] does contain [1, 2]", "error message %s", "formatted") +func (a *Assertions) Subsetf(list interface{}, subset interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Subsetf(a.t, list, subset, msg, args...) +} + +// True asserts that the specified value is true. +// +// a.True(myBool) +func (a *Assertions) True(value bool, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + True(a.t, value, msgAndArgs...) +} + +// Truef asserts that the specified value is true. +// +// a.Truef(myBool, "error message %s", "formatted") +func (a *Assertions) Truef(value bool, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Truef(a.t, value, msg, args...) +} + +// WithinDuration asserts that the two times are within duration delta of each other. +// +// a.WithinDuration(time.Now(), time.Now(), 10*time.Second) +func (a *Assertions) WithinDuration(expected time.Time, actual time.Time, delta time.Duration, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + WithinDuration(a.t, expected, actual, delta, msgAndArgs...) +} + +// WithinDurationf asserts that the two times are within duration delta of each other. +// +// a.WithinDurationf(time.Now(), time.Now(), 10*time.Second, "error message %s", "formatted") +func (a *Assertions) WithinDurationf(expected time.Time, actual time.Time, delta time.Duration, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + WithinDurationf(a.t, expected, actual, delta, msg, args...) +} + +// Zero asserts that i is the zero value for its type. +func (a *Assertions) Zero(i interface{}, msgAndArgs ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Zero(a.t, i, msgAndArgs...) +} + +// Zerof asserts that i is the zero value for its type. +func (a *Assertions) Zerof(i interface{}, msg string, args ...interface{}) { + if h, ok := a.t.(tHelper); ok { + h.Helper() + } + Zerof(a.t, i, msg, args...) +} diff --git a/vendor/github.com/stretchr/testify/require/requirements.go b/vendor/github.com/stretchr/testify/require/requirements.go new file mode 100644 index 000000000..6b85c5ece --- /dev/null +++ b/vendor/github.com/stretchr/testify/require/requirements.go @@ -0,0 +1,29 @@ +package require + +// TestingT is an interface wrapper around *testing.T +type TestingT interface { + Errorf(format string, args ...interface{}) + FailNow() +} + +type tHelper interface { + Helper() +} + +// ComparisonAssertionFunc is a common function prototype when comparing two values. Can be useful +// for table driven tests. +type ComparisonAssertionFunc func(TestingT, interface{}, interface{}, ...interface{}) + +// ValueAssertionFunc is a common function prototype when validating a single value. Can be useful +// for table driven tests. +type ValueAssertionFunc func(TestingT, interface{}, ...interface{}) + +// BoolAssertionFunc is a common function prototype when validating a bool value. Can be useful +// for table driven tests. +type BoolAssertionFunc func(TestingT, bool, ...interface{}) + +// ErrorAssertionFunc is a common function prototype when validating an error value. Can be useful +// for table driven tests. +type ErrorAssertionFunc func(TestingT, error, ...interface{}) + +//go:generate go run ../_codegen/main.go -output-package=require -template=require.go.tmpl -include-format-funcs diff --git a/vendor/github.com/containerd/cri/LICENSE b/vendor/k8s.io/component-base/LICENSE similarity index 99% rename from vendor/github.com/containerd/cri/LICENSE rename to vendor/k8s.io/component-base/LICENSE index 8dada3eda..d64569567 100644 --- a/vendor/github.com/containerd/cri/LICENSE +++ b/vendor/k8s.io/component-base/LICENSE @@ -1,3 +1,4 @@ + Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -178,7 +179,7 @@ APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" + boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a @@ -186,7 +187,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright {yyyy} {name of copyright owner} + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/vendor/k8s.io/component-base/README.md b/vendor/k8s.io/component-base/README.md new file mode 100644 index 000000000..0b6d35c64 --- /dev/null +++ b/vendor/k8s.io/component-base/README.md @@ -0,0 +1,34 @@ +## component-base + +## Purpose + +Implement KEP 32: https://github.com/kubernetes/enhancements/blob/master/keps/sig-cluster-lifecycle/wgs/0032-create-a-k8s-io-component-repo.md + +The proposal is essentially about refactoring the Kubernetes core package structure in a way that all core components may share common code around: + - ComponentConfig implementation + - flag and command handling + - HTTPS serving + - delegated authn/z + - logging. + +## Compatibility + +There are *NO compatibility guarantees* for this repository, yet. It is in direct support of Kubernetes, so branches +will track Kubernetes and be compatible with that repo. As we more cleanly separate the layers, we will review the +compatibility guarantee. We have a goal to make this easier to use in the future. + + +## Where does it come from? + +This repository is synced from https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/component-base. +Code changes are made in that location, merged into `k8s.io/kubernetes` and later synced here. + +## Things you should *NOT* do + + 1. Directly modify any files in this repo. Those are driven from `k8s.io/kubernetes/staging/src/k8s.io/component-base`. + 2. Expect compatibility. This repo is changing quickly in direct support of Kubernetes. + +### OWNERS + +WG Component Standard is working on this refactoring process, which is happening incrementally, starting in the v1.14 cycle. +SIG API Machinery and SIG Cluster Lifecycle owns the code. diff --git a/vendor/k8s.io/component-base/go.mod b/vendor/k8s.io/component-base/go.mod new file mode 100644 index 000000000..81833c480 --- /dev/null +++ b/vendor/k8s.io/component-base/go.mod @@ -0,0 +1,33 @@ +// This is a generated file. Do not edit directly. + +module k8s.io/component-base + +go 1.15 + +require ( + github.com/blang/semver v3.5.0+incompatible + github.com/go-logr/logr v0.2.0 + github.com/google/go-cmp v0.4.0 + github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect + github.com/moby/term v0.0.0-20200312100748-672ec06f55cd + github.com/prometheus/client_golang v1.7.1 + github.com/prometheus/client_model v0.2.0 + github.com/prometheus/common v0.10.0 + github.com/prometheus/procfs v0.1.3 + github.com/sirupsen/logrus v1.6.0 // indirect + github.com/spf13/pflag v1.0.5 + github.com/stretchr/testify v1.4.0 + go.uber.org/atomic v1.4.0 // indirect + go.uber.org/multierr v1.1.0 // indirect + go.uber.org/zap v1.10.0 + k8s.io/apimachinery v0.19.2 + k8s.io/client-go v0.19.2 + k8s.io/klog/v2 v2.2.0 + k8s.io/utils v0.0.0-20200729134348-d5654de09c73 +) + +replace ( + k8s.io/api => k8s.io/api v0.19.2 + k8s.io/apimachinery => k8s.io/apimachinery v0.19.2 + k8s.io/client-go => k8s.io/client-go v0.19.2 +) diff --git a/vendor/k8s.io/component-base/logs/logreduction/logreduction.go b/vendor/k8s.io/component-base/logs/logreduction/logreduction.go new file mode 100644 index 000000000..6534a5a64 --- /dev/null +++ b/vendor/k8s.io/component-base/logs/logreduction/logreduction.go @@ -0,0 +1,78 @@ +/* +Copyright 2018 The Kubernetes 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 + + http://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. +*/ + +package logreduction + +import ( + "sync" + "time" +) + +var nowfunc = func() time.Time { return time.Now() } + +// LogReduction provides a filter for consecutive identical log messages; +// a message will be printed no more than once per interval. +// If a string of messages is interrupted by a different message, +// the interval timer will be reset. +type LogReduction struct { + lastError map[string]string + errorPrinted map[string]time.Time + errorMapLock sync.Mutex + identicalErrorDelay time.Duration +} + +// NewLogReduction returns an initialized LogReduction +func NewLogReduction(identicalErrorDelay time.Duration) *LogReduction { + l := new(LogReduction) + l.lastError = make(map[string]string) + l.errorPrinted = make(map[string]time.Time) + l.identicalErrorDelay = identicalErrorDelay + return l +} + +func (l *LogReduction) cleanupErrorTimeouts() { + for name, timeout := range l.errorPrinted { + if nowfunc().Sub(timeout) >= l.identicalErrorDelay { + delete(l.errorPrinted, name) + delete(l.lastError, name) + } + } +} + +// ShouldMessageBePrinted determines whether a message should be printed based +// on how long ago this particular message was last printed +func (l *LogReduction) ShouldMessageBePrinted(message string, parentID string) bool { + l.errorMapLock.Lock() + defer l.errorMapLock.Unlock() + l.cleanupErrorTimeouts() + lastMsg, ok := l.lastError[parentID] + lastPrinted, ok1 := l.errorPrinted[parentID] + if !ok || !ok1 || message != lastMsg || nowfunc().Sub(lastPrinted) >= l.identicalErrorDelay { + l.errorPrinted[parentID] = nowfunc() + l.lastError[parentID] = message + return true + } + return false +} + +// ClearID clears out log reduction records pertaining to a particular parent +// (e. g. container ID) +func (l *LogReduction) ClearID(parentID string) { + l.errorMapLock.Lock() + defer l.errorMapLock.Unlock() + delete(l.lastError, parentID) + delete(l.errorPrinted, parentID) +} diff --git a/vendor/k8s.io/cri-api/pkg/apis/services.go b/vendor/k8s.io/cri-api/pkg/apis/services.go new file mode 100644 index 000000000..9a22ecbf0 --- /dev/null +++ b/vendor/k8s.io/cri-api/pkg/apis/services.go @@ -0,0 +1,119 @@ +/* +Copyright 2016 The Kubernetes 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 + + http://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. +*/ + +package cri + +import ( + "time" + + runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1alpha2" +) + +// RuntimeVersioner contains methods for runtime name, version and API version. +type RuntimeVersioner interface { + // Version returns the runtime name, runtime version and runtime API version + Version(apiVersion string) (*runtimeapi.VersionResponse, error) +} + +// ContainerManager contains methods to manipulate containers managed by a +// container runtime. The methods are thread-safe. +type ContainerManager interface { + // CreateContainer creates a new container in specified PodSandbox. + CreateContainer(podSandboxID string, config *runtimeapi.ContainerConfig, sandboxConfig *runtimeapi.PodSandboxConfig) (string, error) + // StartContainer starts the container. + StartContainer(containerID string) error + // StopContainer stops a running container with a grace period (i.e., timeout). + StopContainer(containerID string, timeout int64) error + // RemoveContainer removes the container. + RemoveContainer(containerID string) error + // ListContainers lists all containers by filters. + ListContainers(filter *runtimeapi.ContainerFilter) ([]*runtimeapi.Container, error) + // ContainerStatus returns the status of the container. + ContainerStatus(containerID string) (*runtimeapi.ContainerStatus, error) + // UpdateContainerResources updates the cgroup resources for the container. + UpdateContainerResources(containerID string, resources *runtimeapi.LinuxContainerResources) error + // ExecSync executes a command in the container, and returns the stdout output. + // If command exits with a non-zero exit code, an error is returned. + ExecSync(containerID string, cmd []string, timeout time.Duration) (stdout []byte, stderr []byte, err error) + // Exec prepares a streaming endpoint to execute a command in the container, and returns the address. + Exec(*runtimeapi.ExecRequest) (*runtimeapi.ExecResponse, error) + // Attach prepares a streaming endpoint to attach to a running container, and returns the address. + Attach(req *runtimeapi.AttachRequest) (*runtimeapi.AttachResponse, error) + // ReopenContainerLog asks runtime to reopen the stdout/stderr log file + // for the container. If it returns error, new container log file MUST NOT + // be created. + ReopenContainerLog(ContainerID string) error +} + +// PodSandboxManager contains methods for operating on PodSandboxes. The methods +// are thread-safe. +type PodSandboxManager interface { + // RunPodSandbox creates and starts a pod-level sandbox. Runtimes should ensure + // the sandbox is in ready state. + RunPodSandbox(config *runtimeapi.PodSandboxConfig, runtimeHandler string) (string, error) + // StopPodSandbox stops the sandbox. If there are any running containers in the + // sandbox, they should be force terminated. + StopPodSandbox(podSandboxID string) error + // RemovePodSandbox removes the sandbox. If there are running containers in the + // sandbox, they should be forcibly removed. + RemovePodSandbox(podSandboxID string) error + // PodSandboxStatus returns the Status of the PodSandbox. + PodSandboxStatus(podSandboxID string) (*runtimeapi.PodSandboxStatus, error) + // ListPodSandbox returns a list of Sandbox. + ListPodSandbox(filter *runtimeapi.PodSandboxFilter) ([]*runtimeapi.PodSandbox, error) + // PortForward prepares a streaming endpoint to forward ports from a PodSandbox, and returns the address. + PortForward(*runtimeapi.PortForwardRequest) (*runtimeapi.PortForwardResponse, error) +} + +// ContainerStatsManager contains methods for retrieving the container +// statistics. +type ContainerStatsManager interface { + // ContainerStats returns stats of the container. If the container does not + // exist, the call returns an error. + ContainerStats(containerID string) (*runtimeapi.ContainerStats, error) + // ListContainerStats returns stats of all running containers. + ListContainerStats(filter *runtimeapi.ContainerStatsFilter) ([]*runtimeapi.ContainerStats, error) +} + +// RuntimeService interface should be implemented by a container runtime. +// The methods should be thread-safe. +type RuntimeService interface { + RuntimeVersioner + ContainerManager + PodSandboxManager + ContainerStatsManager + + // UpdateRuntimeConfig updates runtime configuration if specified + UpdateRuntimeConfig(runtimeConfig *runtimeapi.RuntimeConfig) error + // Status returns the status of the runtime. + Status() (*runtimeapi.RuntimeStatus, error) +} + +// ImageManagerService interface should be implemented by a container image +// manager. +// The methods should be thread-safe. +type ImageManagerService interface { + // ListImages lists the existing images. + ListImages(filter *runtimeapi.ImageFilter) ([]*runtimeapi.Image, error) + // ImageStatus returns the status of the image. + ImageStatus(image *runtimeapi.ImageSpec) (*runtimeapi.Image, error) + // PullImage pulls an image with the authentication config. + PullImage(image *runtimeapi.ImageSpec, auth *runtimeapi.AuthConfig, podSandboxConfig *runtimeapi.PodSandboxConfig) (string, error) + // RemoveImage removes the image. + RemoveImage(image *runtimeapi.ImageSpec) error + // ImageFsInfo returns information of the filesystem that is used to store images. + ImageFsInfo() ([]*runtimeapi.FilesystemUsage, error) +}