Merge branch 'main' into feature/migrate-operator-sdk-dependency

This commit is contained in:
Marton Soos
2022-04-15 12:00:50 +02:00
10 changed files with 741 additions and 0 deletions

36
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,36 @@
---
name: Bug report
about: Report bugs and errors found while using the Operator.
title: ''
labels: bug
assignees: ''
---
### Your environment
<!-- Version of the Operator when the error occurred -->
Operator Version:
<!-- What version of the Connect server are you running?
You can get this information from the Integrations section in 1Password
https://start.1password.com/integrations/active
-->
Connect Server Version:
<!-- What version of Kubernetes have you deployed the operator to? -->
Kubernetes Version:
## What happened?
<!-- Describe the bug or error -->
## What did you expect to happen?
<!-- Describe what should have happened -->
## Steps to reproduce
1. <!-- Describe Steps to reproduce the issue -->
## Notes & Logs
<!-- Paste any logs here that may help with debugging.
Remember to remove any sensitive information before sharing! -->

9
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,9 @@
# docs: https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/configuring-issue-templates-for-your-repository#configuring-the-template-chooser
blank_issues_enabled: true
contact_links:
- name: 1Password Community
url: https://1password.community/categories/secrets-automation
about: Please ask general Secrets Automation questions here.
- name: 1Password Security Bug Bounty
url: https://bugcrowd.com/agilebits
about: Please report security vulnerabilities here.

View File

@@ -0,0 +1,32 @@
---
name: Feature request
about: Suggest an idea for the Operator
title: ''
labels: feature-request
assignees: ''
---
### Summary
<!-- Briefly describe the feature in one or two sentences. You can include more details later. -->
### Use cases
<!-- Describe the use cases that make this feature useful to others.
The description should help the reader understand why the feature is necessary.
The better we understand your use case, the better we can help create an appropriate solution. -->
### Proposed solution
<!-- If you already have an idea for how the feature should work, use this space to describe it.
We'll work with you to find a workable approach, and any implementation details are appreciated.
-->
### Is there a workaround to accomplish this today?
<!-- If there's a way to accomplish this feature request without changes to the codebase, we'd like to hear it.
-->
### References & Prior Work
<!-- If a similar feature was implemented in another project or tool, add a link so we can better understand your request.
Links to relevant documentation or RFCs are also appreciated. -->
* <!-- Reference 1 -->
* <!-- Reference 2, etc -->

21
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,21 @@
name: Build and Test
on: [push, pull_request]
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Set up Go 1.x
uses: actions/setup-go@v2
with:
go-version: ^1.15
- name: Check out code into the Go module directory
uses: actions/checkout@v2
- name: Build
run: go build -v ./...
- name: Test
run: go test -v ./... -cover

85
.github/workflows/release-pr.yml vendored Normal file
View File

@@ -0,0 +1,85 @@
on:
create:
branches:
name: Open Release PR for review
jobs:
# This job is necessary because GitHub does not (yet) support
# filtering `create` triggers by branch name.
# See: https://github.community/t/trigger-job-on-branch-created/16878/5
should_create_pr:
name: Check if PR for branch already exists
runs-on: ubuntu-latest
outputs:
result: ${{ steps.is_release_branch_without_pr.outputs.result }}
steps:
- id: is_release_branch_without_pr
name: Find matching PR
uses: actions/github-script@v3
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
// Search for an existing PR with head & base
// that match the created branch
const [releaseBranchName] = context.ref.match("release/v[0-9]+\.[0-9]+\.[0-9]+") || []
if(!releaseBranchName) { return false }
const {data: prs} = await github.pulls.list({
...context.repo,
state: 'open',
head: `1Password:${releaseBranchName}`,
base: context.payload.master_branch
})
return prs.length === 0
create_pr:
needs: should_create_pr
if: needs.should_create_pr.outputs.result == 'true'
name: Create Release Pull Request
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Parse release version
id: get_version
run: echo "::set-output name=version::$(echo $GITHUB_REF | sed 's|^refs/heads/release/v?*||g')"
- name: Prepare Pull Request
id: prep_pr
run: |
CHANGELOG_PATH=$(printf "%s/CHANGELOG.md" "${GITHUB_WORKSPACE}")
LOG_ENTRY=$(awk '/START\/v[0-9]+\.[0-9]+\.[0-9]+*/{f=1; next} /---/{if (f == 1) exit} f' "${CHANGELOG_PATH}")
export PR_BODY=$(cat <<EOF
This is an automated PR for a new release.
Please check the following before approving:
- [ ] Changelog is accurate. The documented changes for this release are printed below.
- [ ] Any files referencing a version number. Confirm it matches the version number in the branch name.
---
## Release Changelog Preview
${LOG_ENTRY}
EOF
)
# Sanitizes multiline strings for action outputs (https://medium.com/agorapulse-stories/23f56447d209)
PR_BODY="${PR_BODY//'%'/'%25'}"
PR_BODY="${PR_BODY//$'\n'/'%0A'}"
PR_BODY="${PR_BODY//$'\r'/'%0D'}"
echo "::set-output name=pr_body::$(echo "$PR_BODY")"
- name: Create Pull Request via API
id: post_pr
uses: octokit/request-action@v2.x
with:
route: POST /repos/${{ github.repository }}/pulls
title: ${{ format('Prepare Release - v{0}', steps.get_version.outputs.version) }}
head: ${{ github.ref }}
base: ${{ github.event.master_branch }}
body: ${{ toJson(steps.prep_pr.outputs.pr_body) }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

57
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,57 @@
name: release
on:
push:
tags:
- 'v*'
jobs:
release-docker:
runs-on: ubuntu-latest
env:
DOCKER_CLI_EXPERIMENTAL: "enabled"
steps:
-
name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
-
name: Docker meta
id: meta
uses: crazy-max/ghaction-docker-meta@v2
with:
images: |
1password/onepassword-operator
# Publish image for x.y.z and x.y
# The latest tag is automatically added for semver tags
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
- name: Get the version from tag
id: get_version
run: echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/v}
-
name: Set up QEMU
uses: docker/setup-qemu-action@v1
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
-
name: Docker Login
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
-
name: Build and push
uses: docker/build-push-action@v2
with:
context: .
file: Dockerfile
platforms: linux/amd64,linux/arm64,linux/arm/v7
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
operator_version=${{ steps.get_version.outputs.VERSION }}

135
CHANGELOG.md Normal file
View File

@@ -0,0 +1,135 @@
[//]: # (START/LATEST)
# Latest
## Features
* A user-friendly description of a new feature. {issue-number}
## Fixes
* A user-friendly description of a fix. {issue-number}
## Security
* A user-friendly description of a security fix. {issue-number}
---
[//]: # "START/v1.4.1"
# v1.4.1
## Fixes
- OwnerReferences on secrets are now persisted after an item is updated. {#101}
- Annotations from a Deployment or OnePasswordItem are no longer applied to Secrets that are created for it. {#102}
---
[//]: # "START/v1.4.0"
# v1.4.0
## Features
- The operator now declares the an OwnerReference for the secrets it manages. This should stop secrets from getting pruned by tools like Argo CD. {#51,#84,#96}
---
[//]: # "START/v1.3.0"
# v1.3.0
## Features
- Added support for loading secrets from files stored in 1Password. {#47}
---
[//]: # "START/v1.2.0"
# v1.2.0
## Features
- Support secrets provisioned through FromEnv. {#74}
- Support configuration of Kubernetes Secret type. {#87}
- Improved logging. (#72)
---
[//]: # "START/v1.1.0"
# v1.1.0
## Fixes
- Fix normalization for keys in a Secret's `data` section to allow upper- and lower-case alphanumeric characters. {#66}
---
[//]: # "START/v1.0.2"
# v1.0.2
## Fixes
- Name normalizer added to handle non-conforming item names.
---
[//]: # "START/v1.0.1"
# v1.0.1
## Features
- This release also contains an arm64 Docker image. {#20}
- Docker images are also pushed to the :latest and :<major>.<minor> tags.
---
[//]: # "START/v1.0.0"
# v1.0.0
## Features:
- Option to automatically deploy 1Password Connect via the operator
- Ignore restart annotation when looking for 1Password annotations
- Release Automation
- Upgrading apiextensions.k8s.io/v1beta apiversion from the operator custom resource
- Adding configuration for auto rolling restart on deployments
- Configure Auto Restarts for a OnePasswordItem Custom Resource
- Update Connect Dependencies to latest
- Add Github action for building and testing operator
## Fixes:
- Fix spec field example for OnePasswordItem in readme
- Casing of annotations are now consistent
---
[//]: # "START/v0.0.2"
# v0.0.2
## Features:
- Items can now be accessed by either `vaults/<vault_id>/items/<item_id>` or `vaults/<vault_title>/items/<item_title>`
---
[//]: # "START/v0.0.1"
# v0.0.1
Initial 1Password Operator release
## Features
- watches for deployment creations with `onepassword` annotations and creates an associated kubernetes secret
- watches for `onepasswordsecret` crd creations and creates an associated kubernetes secrets
- watches for changes to 1Password secrets associated with kubernetes secrets and updates the kubernetes secret with changes
- restart pods when secret has been updated
- cleanup of kubernetes secrets when deployment or `onepasswordsecret` is deleted
---

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 1Password
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.

241
README.md Normal file
View File

@@ -0,0 +1,241 @@
// TODO: Update README.md
# 1Password Connect Kubernetes Operator
The 1Password Connect Kubernetes Operator provides the ability to integrate Kubernetes with 1Password. This Operator manages `OnePasswordItem` Custom Resource Definitions (CRDs) that define the location of an Item stored in 1Password. The `OnePasswordItem` CRD, when created, will be used to compose a Kubernetes Secret containing the contents of the specified item.
The 1Password Connect Kubernetes Operator also allows for Kubernetes Secrets to be composed from a 1Password Item through annotation of an Item Path on a deployment.
The 1Password Connect Kubernetes Operator will continually check for updates from 1Password for any Kubernetes Secret that it has generated. If a Kubernetes Secret is updated, any Deployment using that secret can be automatically restarted.
## Setup
Prerequisites:
- [1Password Command Line Tool Installed](https://1password.com/downloads/command-line/)
- [kubectl installed](https://kubernetes.io/docs/tasks/tools/install-kubectl/)
- [docker installed](https://docs.docker.com/get-docker/)
- [Generated a 1password-credentials.json file and issued a 1Password Connect API Token for the K8s Operator integration](https://support.1password.com/secrets-automation/)
- [1Password Connect deployed to Kubernetes](https://support.1password.com/connect-deploy-kubernetes/#step-2-deploy-a-1password-connect-server). **NOTE**: If customization of the 1Password Connect deployment is not required you can skip this prerequisite.
### Quickstart for Deploying 1Password Connect to Kubernetes
#### Deploy with Helm
The 1Password Connect Helm Chart helps to simplify the deployment of 1Password Connect and the 1Password Connect Kubernetes Operator to Kubernetes.
[The 1Password Connect Helm Chart can be found here.](https://github.com/1Password/connect-helm-charts)
#### Deploy using the Connect Operator
If 1Password Connect is already running, you can skip this step. This guide will provide a quickstart option for deploying a default configuration of 1Password Connect via starting the deploying the 1Password Connect Operator, however it is recommended that you instead deploy your own manifest file if customization of the 1Password Connect deployment is desired.
Encode the 1password-credentials.json file you generated in the prerequisite steps and save it to a file named op-session:
```bash
cat 1password-credentials.json | base64 | \
tr '/+' '_-' | tr -d '=' | tr -d '\n' > op-session
```
Create a Kubernetes secret from the op-session file:
```bash
kubectl create secret generic op-credentials --from-file=op-session
```
Add the following environment variable to the onepassword-connect-operator container in `deploy/operator.yaml`:
```yaml
- name: MANAGE_CONNECT
value: "true"
```
Adding this environment variable will have the operator automatically deploy a default configuration of 1Password Connect to the `default` namespace.
### Kubernetes Operator Deployment
**Create Kubernetes Secret for OP_CONNECT_TOKEN**
"Create a Connect token for the operator and save it as a Kubernetes Secret:
```bash
kubectl create secret generic onepassword-token --from-literal=token=<OP_CONNECT_TOKEN>"
```
If you do not have a token for the operator, you can generate a token and save it to kubernetes with the following command:
```bash
kubectl create secret generic onepassword-token --from-literal=token=$(op create connect token <server> op-k8s-operator --vault <vault>)
```
[More information on generating a token can be found here](https://support.1password.com/secrets-automation/#appendix-issue-additional-access-tokens)
**Set Permissions For Operator**
We must create a service account, role, and role binding and Kubernetes. Examples can be found in the `/deploy` folder.
```bash
kubectl apply -f deploy/permissions.yaml
```
**Create Custom One Password Secret Resource**
```bash
kubectl apply -f deploy/crds/onepassword.com_onepassworditems_crd.yaml
```
**Deploying the Operator**
An sample Deployment yaml can be found at `/deploy/operator.yaml`.
To further configure the 1Password Kubernetes Operator the Following Environment variables can be set in the operator yaml:
- **OP_CONNECT_HOST** (required): Specifies the host name within Kubernetes in which to access the 1Password Connect.
- **WATCH_NAMESPACE:** (default: watch all namespaces): Comma separated list of what Namespaces to watch for changes.
- **POLLING_INTERVAL** (default: 600): The number of seconds the 1Password Kubernetes Operator will wait before checking for updates from 1Password Connect.
- **MANAGE_CONNECT** (default: false): If set to true, on deployment of the operator, a default configuration of the OnePassword Connect Service will be deployed to the `default` namespace.
- **AUTO_RESTART** (default: false): If set to true, the operator will restart any deployment using a secret from 1Password Connect. This can be overwritten by namespace, deployment, or individual secret. More details on AUTO_RESTART can be found in the ["Configuring Automatic Rolling Restarts of Deployments"](#configuring-automatic-rolling-restarts-of-deployments) section.
Apply the deployment file:
```yaml
kubectl apply -f deploy/operator.yaml
```
## Usage
To create a Kubernetes Secret from a 1Password item, create a yaml file with the following
```yaml
apiVersion: onepassword.com/v1
kind: OnePasswordItem
metadata:
name: <item_name> #this name will also be used for naming the generated kubernetes secret
spec:
itemPath: "vaults/<vault_id_or_title>/items/<item_id_or_title>"
```
Deploy the OnePasswordItem to Kubernetes:
```bash
kubectl apply -f <your_item>.yaml
```
To test that the Kubernetes Secret check that the following command returns a secret:
```bash
kubectl get secret <secret_name>
```
Note: Deleting the `OnePasswordItem` that you've created will automatically delete the created Kubernetes Secret.
To create a single Kubernetes Secret for a deployment, add the following annotations to the deployment metadata:
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: deployment-example
annotations:
operator.1password.io/item-path: "vaults/<vault_id_or_title>/items/<item_id_or_title>"
operator.1password.io/item-name: "<secret_name>"
```
Applying this yaml file will create a Kubernetes Secret with the name `<secret_name>` and contents from the location specified at the specified Item Path.
The contents of the Kubernetes secret will be key-value pairs in which the keys are the fields of the 1Password item and the values are the corresponding values stored in 1Password.
In case of fields that store files, the file's contents will be used as the value.
Within an item, if both a field storing a file and a field of another type have the same name, the file field will be ignored and the other field will take precedence.
Note: Deleting the Deployment that you've created will automatically delete the created Kubernetes Secret only if the deployment is still annotated with `operator.1password.io/item-path` and `operator.1password.io/item-name` and no other deployment is using the secret.
If a 1Password Item that is linked to a Kubernetes Secret is updated within the POLLING_INTERVAL the associated Kubernetes Secret will be updated. However, if you do not want a specific secret to be updated you can add the tag `operator.1password.io:ignore-secret` to the item stored in 1Password. While this tag is in place, any updates made to an item will not trigger an update to the associated secret in Kubernetes.
---
**NOTE**
If multiple 1Password vaults/items have the same `title` when using a title in the access path, the desired action will be performed on the oldest vault/item.
Titles and field names that include white space and other characters that are not a valid [DNS subdomain name](https://kubernetes.io/docs/concepts/configuration/secret/) will create Kubernetes secrets that have titles and fields in the following format:
- Invalid characters before the first alphanumeric character and after the last alphanumeric character will be removed
- All whitespaces between words will be replaced by `-`
- All the letters will be lower-cased.
---
### Configuring Automatic Rolling Restarts of Deployments
If a 1Password Item that is linked to a Kubernetes Secret is updated, any deployments configured to `auto-restart` AND are using that secret will be given a rolling restart the next time 1Password Connect is polled for updates.
There are many levels of granularity on which to configure auto restarts on deployments: at the operator level, per-namespace, or per-deployment.
**On the operator**: This method allows for managing auto restarts on all deployments within the namespaces watched by operator. Auto restarts can be enabled by setting the environemnt variable `AUTO_RESTART` to true. If the value is not set, the operator will default this value to false.
**Per Namespace**: This method allows for managing auto restarts on all deployments within a namespace. Auto restarts can by managed by setting the annotation `operator.1password.io/auto-restart` to either `true` or `false` on the desired namespace. An example of this is shown below:
```yaml
# enabled auto restarts for all deployments within a namespace unless overwritten within a deployment
apiVersion: v1
kind: Namespace
metadata:
name: "example-namespace"
annotations:
operator.1password.io/auto-restart: "true"
```
If the value is not set, the auto restart settings on the operator will be used. This value can be overwritten by deployment.
**Per Deployment**
This method allows for managing auto restarts on a given deployment. Auto restarts can by managed by setting the annotation `operator.1password.io/auto-restart` to either `true` or `false` on the desired deployment. An example of this is shown below:
```yaml
# enabled auto restarts for the deployment
apiVersion: v1
kind: Deployment
metadata:
name: "example-deployment"
annotations:
operator.1password.io/auto-restart: "true"
```
If the value is not set, the auto restart settings on the namespace will be used.
**Per OnePasswordItem Custom Resource**
This method allows for managing auto restarts on a given OnePasswordItem custom resource. Auto restarts can by managed by setting the annotation `operator.1password.io/auto_restart` to either `true` or `false` on the desired OnePasswordItem. An example of this is shown below:
```yaml
# enabled auto restarts for the OnePasswordItem
apiVersion: onepassword.com/v1
kind: OnePasswordItem
metadata:
name: example
annotations:
operator.1password.io/auto-restart: "true"
```
If the value is not set, the auto restart settings on the deployment will be used.
## Development
### Creating a Docker image
To create a local version of the Docker image for testing, use the following `Makefile` target:
```shell
make build/local
```
### Building the Operator binary
```shell
make build/binary
```
The binary will be placed inside a `dist` folder within this repository.
### Running Tests
```shell
make test
```
With coverage:
```shell
make test/coverage
```
## Security
1Password requests you practice responsible disclosure if you discover a vulnerability.
Please file requests via [**BugCrowd**](https://bugcrowd.com/agilebits).
For information about security practices, please visit our [Security homepage](https://bugcrowd.com/agilebits).

104
scripts/prepare-release.sh Executable file
View File

@@ -0,0 +1,104 @@
#!/usr/bin/env bash
#
# prepare-release.sh
# (Note: This should be called by `make release/prepare` because it depends
# on several variables set by the Makefile)
#
# Performs release preparation tasks:
# - Creates a release branch
# - Renames "LATEST" section to the new version number
# - Adds new "LATEST" entry to the changelog
#
##############################################
set -Eeuo pipefail
if [[ -z "${NEW_VERSION:-}" ]]; then
echo "[ERROR] NEW_VERSION environment variable not defined." >&2
exit 1
fi
# Script called from within a git repo?
if [[ $(git rev-parse --is-inside-work-tree &>/dev/null) -ne 0 ]]; then
echo "[ERROR] Current directory (${SRCDIR}) is not a git repository" >&2
exit 1
fi
REPO_ROOT=$(git rev-parse --show-toplevel)
CHANGELOG_FILENAME=${CHANGELOG:-"CHANGELOG.md"}
# normalize version by removing `v` prefix
VERSION_NUM=${NEW_VERSION/#v/}
RELEASE_BRANCH=$(printf "release/v%s" "${VERSION_NUM}")
function updateChangelog() {
local tmpfile
trap '[ -e "${tmpfile}" ] && rm "${tmpfile}"' RETURN
local changelogFile
changelogFile=$(printf "%s/%s" "${REPO_ROOT}" "${CHANGELOG_FILENAME}")
# create Changelog file if not exists
if ! [[ -f "${REPO_ROOT}/${CHANGELOG_FILENAME}" ]]; then
touch "${REPO_ROOT}/${CHANGELOG_FILENAME}" && \
git add "${REPO_ROOT}/${CHANGELOG_FILENAME}"
fi
tmpfile=$(mktemp)
# Replace "Latest" in the top-most changelog block with new version
# Then push a new "latest" block to top of the changelog
awk 'NR==1, /---/{ sub(/START\/LATEST/, "START/v'${VERSION_NUM}'"); sub(/# Latest/, "# v'${VERSION_NUM}'") } {print}' \
"${changelogFile}" > "${tmpfile}"
# Inserts "Latest" changelog HEREDOC at the top of the file
cat - "${tmpfile}" << EOF > "${REPO_ROOT}/${CHANGELOG_FILENAME}"
[//]: # (START/LATEST)
# Latest
## Features
* A user-friendly description of a new feature. {issue-number}
## Fixes
* A user-friendly description of a fix. {issue-number}
## Security
* A user-friendly description of a security fix. {issue-number}
---
EOF
}
function _main() {
# Stash version changes
git stash push &>/dev/null
if ! git checkout -b "${RELEASE_BRANCH}" origin/"${MAIN_BRANCH:-main}"; then
echo "[ERROR] Could not check out release branch." >&2
git stash pop &>/dev/null
exit 1
fi
# Add the version changes to release branch
git stash pop &>/dev/null
updateChangelog
cat << EOF
[SUCCESS] Changelog updated & release branch created:
New Version: ${NEW_VERSION}
Release Branch: ${RELEASE_BRANCH}
Next steps:
1. Edit the changelog notes in ${CHANGELOG_FILENAME}
2. Commit changes to the release branch
3. Push changes to remote => git push origin ${RELEASE_BRANCH}
EOF
exit 0
}
_main