Compare commits

...

19 Commits

Author SHA1 Message Date
itsconquest
344bf1082a Merge remote-tracking branch 'origin/develop' into e-2747-docker-desktop-extension 2022-04-12 13:15:37 +12:00
itsconquest
de9bdf1cfa fix 2 failing FE tests [EE-2938] 2022-04-12 10:39:37 +12:00
Stéphane Busso
6afb359af6 Update app/portainer/components/PageHeader/HeaderContent.html
Co-authored-by: Chaim Lev-Ari <chiptus@users.noreply.github.com>
2022-04-11 20:34:26 +12:00
Stéphane Busso
043b341e22 Move default to constants 2022-04-11 19:49:03 +12:00
Stéphane Busso
a210791414 Hide only username 2022-04-11 19:37:47 +12:00
Stéphane Busso
1719810ae7 FIxes base path 2022-04-11 19:33:49 +12:00
Stéphane Busso
75297d0ed6 Password manageListen on locahost onlyr 2022-04-11 19:21:01 +12:00
itsconquest
b6483a9e36 fix local dev image + hide users & auth 2022-04-11 15:43:07 +12:00
itsconquest
0f8f7b2051 Restore version variable and add local install command 2022-04-11 15:03:09 +12:00
itsconquest
efd9391496 change to building locally 2022-04-11 14:29:56 +12:00
Stéphane Busso
7e05935131 Password manager 2022-04-08 17:29:08 +12:00
Stéphane Busso
76318fbb54 Fix hash test 2022-04-08 14:47:26 +12:00
Stéphane Busso
68a7852295 Remove space 2022-04-08 13:35:11 +12:00
Stéphane Busso
8b330176ab Tidy mod 2022-04-08 13:30:52 +12:00
Stéphane Busso
fd28b66fc5 move build and remove https 2022-04-08 13:29:44 +12:00
Stéphane Busso
5f37097ba2 Fix kube shell 2022-04-08 13:29:44 +12:00
Stéphane Busso
2c6360521f fix kubeshell baseurl 2022-04-08 13:29:44 +12:00
Stéphane Busso
618e35bbb5 Add auto login
fix auto auth

add some message

Add extension version

Double attempt to login

Add auto login from jwt check

Add autologin on logout

revert sidebar

Catch error 401 to relogin

cleanup login

Add password generator

Hide User block and collapse sidebar by default

hide user box and toggle sidebar

remove defailt dd

Integrate extension to portainer

Move extension to build

remove files from ignore

Move extension folder

fix alpine

try to copy folder

try add

Change base image

move folder extension

ignore folder build

Fix

relative path

Move ext to root

fix image name

versioned index

Update extension on same image

Update mod
2022-04-08 13:29:37 +12:00
Stéphane Busso
33bfa87b83 Initial extension build 2022-04-08 13:22:56 +12:00
38 changed files with 506 additions and 56 deletions

View File

@@ -1,3 +1,5 @@
*
!dist
!build
!metadata.json
!docker-extension/build

View File

@@ -53,11 +53,12 @@ To do so, you can use the `/endpoints/{id}/docker` Portainer API environment(end
# Private Registry
Using private registry, you will need to pass a based64 encoded JSON string {"registryId":\<registryID value\>} inside the Request Header. The parameter name is "X-Registry-Auth".
\<registryID value\> - The registry ID where the repository was created.
\<registryID value\> - The registry ID where the repository was created.
Example:
```
eyJyZWdpc3RyeUlkIjoxfQ==
```
**NOTE**: You can find more information on how to query the Docker API in the [Docker official documentation](https://docs.docker.com/engine/api/v1.30/) as well as in [this Portainer example](https://documentation.portainer.io/api/api-examples/).

View File

@@ -51,8 +51,9 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
SSLKey: kingpin.Flag("sslkey", "Path to the SSL key used to secure the Portainer instance").String(),
Rollback: kingpin.Flag("rollback", "Rollback the database store to the previous version").Bool(),
SnapshotInterval: kingpin.Flag("snapshot-interval", "Duration between each environment snapshot job").String(),
AdminPassword: kingpin.Flag("admin-password", "Hashed admin password").String(),
AdminPassword: kingpin.Flag("admin-password", "Set admin password with provided hash").String(),
AdminPasswordFile: kingpin.Flag("admin-password-file", "Path to the file containing the password for the admin user").String(),
GenerateAdminPassword: kingpin.Flag("generate-admin-password", "Generate a password for the admin user").Bool(),
Labels: pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l')),
Logo: kingpin.Flag("logo", "URL for the logo displayed in the UI").String(),
Templates: kingpin.Flag("templates", "URL to the templates definitions.").Short('t').String(),

View File

@@ -34,6 +34,7 @@ import (
kubeproxy "github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
"github.com/portainer/portainer/api/internal/authorization"
"github.com/portainer/portainer/api/internal/edge"
"github.com/portainer/portainer/api/internal/password"
"github.com/portainer/portainer/api/internal/snapshot"
"github.com/portainer/portainer/api/internal/ssl"
"github.com/portainer/portainer/api/jwt"
@@ -647,6 +648,13 @@ func buildServer(flags *portainer.CLIFlags) portainer.Server {
adminPasswordHash = *flags.AdminPassword
}
if *flags.GenerateAdminPassword {
adminPasswordHash, err = password.GeneratePassword(cryptoService, fileService)
if err != nil {
logrus.Fatalf("Failed generating admin password: %v", err)
}
}
if adminPasswordHash != "" {
users, err := dataStore.User().UsersByRole(portainer.AdministratorRole)
if err != nil {

View File

@@ -9,11 +9,11 @@ type Service struct{}
// Hash hashes a string using the bcrypt algorithm
func (*Service) Hash(data string) (string, error) {
hash, err := bcrypt.GenerateFromPassword([]byte(data), bcrypt.DefaultCost)
bytes, err := bcrypt.GenerateFromPassword([]byte(data), bcrypt.DefaultCost)
if err != nil {
return "", nil
return "", err
}
return string(hash), nil
return string(bytes), err
}
// CompareHashAndData compares a hash to clear data and returns an error if the comparison fails.

53
api/crypto/hash_test.go Normal file
View File

@@ -0,0 +1,53 @@
package crypto
import (
"testing"
)
func TestService_Hash(t *testing.T) {
var s = &Service{}
type args struct {
hash string
data string
}
tests := []struct {
name string
args args
expect bool
}{
{
name: "Empty",
args: args{
hash: "",
data: "",
},
expect: false,
},
{
name: "Matching",
args: args{
hash: "$2a$10$6BFGd94oYx8k0bFNO6f33uPUpcpAJyg8UVX.akLe9EthF/ZBTXqcy",
data: "Passw0rd!",
},
expect: true,
},
{
name: "Not matching",
args: args{
hash: "$2a$10$ltKrUZ7492xyutHOb0/XweevU4jyw7QO66rP32jTVOMb3EX3JxA/a",
data: "Passw0rd!",
},
expect: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := s.CompareHashAndData(tt.args.hash, tt.args.data)
if (err != nil) == tt.expect {
t.Errorf("Service.CompareHashAndData() = %v", err)
}
})
}
}

View File

@@ -7,6 +7,12 @@ import (
"github.com/pkg/errors"
)
// WriteToFile create a file in the filesystem storage
func (service *Service) WriteToFile(path string, data []byte) error {
return WriteToFile(path, data)
}
// WriteToFile create a file in the filesystem storage
func WriteToFile(dst string, content []byte) error {
if err := os.MkdirAll(filepath.Dir(dst), 0744); err != nil {
return errors.Wrapf(err, "failed to create filestructure for the path %q", dst)

View File

@@ -35,6 +35,7 @@ require (
github.com/portainer/libhttp v0.0.0-20211208103139-07a5f798eb3f
github.com/rkl-/digest v0.0.0-20180419075440-8316caa4a777
github.com/robfig/cron/v3 v3.0.1
github.com/sethvargo/go-password v0.2.0
github.com/sirupsen/logrus v1.8.1
github.com/stretchr/testify v1.7.0
github.com/viney-shih/go-lock v1.1.1

View File

@@ -670,6 +670,8 @@ github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdh
github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/sethvargo/go-password v0.2.0 h1:BTDl4CC/gjf/axHMaDQtw507ogrXLci6XRiLc7i/UHI=
github.com/sethvargo/go-password v0.2.0/go.mod h1:Ym4Mr9JXLBycr02MFuVQ/0JHidNetSgbzutTr3zsYXE=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=

View File

@@ -0,0 +1,77 @@
package password
import (
"os"
"github.com/pkg/errors"
portainer "github.com/portainer/portainer/api"
"github.com/sethvargo/go-password/password"
)
const passwordFile = ".pwd"
type service struct {
cryptoService portainer.CryptoService
fileService portainer.FileService
}
// GeneratePassword generates a password and stores it in the filesystem, return a hash of the password
func GeneratePassword(cryptoService portainer.CryptoService, fileService portainer.FileService) (string, error) {
pw := service{
cryptoService: cryptoService,
fileService: fileService,
}
hash, err := pw.getHashFromPasswordFile()
if err == nil {
return hash, nil
}
return pw.generatePassword()
}
func (pw *service) getHashFromPasswordFile() (string, error) {
if !pw.checkPasswordFile() {
return "", errors.New("password file does not exist")
}
pwd, err := pw.loadPassword()
if err != nil {
return "", errors.Wrap(err, "failed to load password")
}
return pw.cryptoService.Hash(string(pwd))
}
func (pw *service) generatePassword() (string, error) {
pwd, err := password.Generate(16, 4, 4, false, false)
if err != nil {
return "", err
}
hash, err := pw.cryptoService.Hash(pwd)
if err != nil {
return "", errors.Wrap(err, "failed to hash password")
}
pw.savePassword(pwd)
pw.fileService.WriteToFile(passwordFile, []byte(hash))
return hash, nil
}
func (pw *service) savePassword(pwd string) error {
return pw.fileService.WriteToFile(passwordFile, []byte(pwd))
}
func (pw *service) loadPassword() ([]byte, error) {
return pw.fileService.GetFileContent("", passwordFile)
}
func (pw *service) checkPasswordFile() bool {
_, err := pw.fileService.FileExists(passwordFile)
return !os.IsNotExist(err)
}

View File

@@ -0,0 +1,142 @@
package password
import (
"reflect"
"testing"
portainer "github.com/portainer/portainer/api"
)
func TestGeneratePassword(t *testing.T) {
type args struct {
cryptoService portainer.CryptoService
fileService portainer.FileService
}
tests := []struct {
name string
args args
want string
wantErr bool
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := GeneratePassword(tt.args.cryptoService, tt.args.fileService)
if (err != nil) != tt.wantErr {
t.Errorf("GeneratePassword() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("GeneratePassword() = %v, want %v", got, tt.want)
}
})
}
}
func Test_service_getHashFromPasswordFile(t *testing.T) {
tests := []struct {
name string
pw *service
want string
wantErr bool
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := tt.pw.getHashFromPasswordFile()
if (err != nil) != tt.wantErr {
t.Errorf("service.getHashFromPasswordFile() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("service.getHashFromPasswordFile() = %v, want %v", got, tt.want)
}
})
}
}
func Test_service_generatePassword(t *testing.T) {
tests := []struct {
name string
pw *service
want string
wantErr bool
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := tt.pw.generatePassword()
if (err != nil) != tt.wantErr {
t.Errorf("service.generatePassword() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("service.generatePassword() = %v, want %v", got, tt.want)
}
})
}
}
func Test_service_savePassword(t *testing.T) {
type args struct {
pwd string
}
tests := []struct {
name string
pw *service
args args
wantErr bool
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := tt.pw.savePassword(tt.args.pwd); (err != nil) != tt.wantErr {
t.Errorf("service.savePassword() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func Test_service_loadPassword(t *testing.T) {
tests := []struct {
name string
pw *service
want []byte
wantErr bool
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := tt.pw.loadPassword()
if (err != nil) != tt.wantErr {
t.Errorf("service.loadPassword() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("service.loadPassword() = %v, want %v", got, tt.want)
}
})
}
}
func Test_service_checkPasswordFile(t *testing.T) {
tests := []struct {
name string
pw *service
want bool
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.pw.checkPasswordFile(); got != tt.want {
t.Errorf("service.checkPasswordFile() = %v, want %v", got, tt.want)
}
})
}
}

View File

@@ -99,6 +99,7 @@ type (
TunnelPort *string
AdminPassword *string
AdminPasswordFile *string
GenerateAdminPassword *bool
Assets *string
Data *string
FeatureFlags *[]Pair
@@ -1240,6 +1241,7 @@ type (
CopySSLCertPair(certPath, keyPath string) (string, string, error)
CopySSLCACert(caCertPath string) (string, error)
StoreFDOProfileFileFromBytes(fdoProfileIdentifier string, data []byte) (string, error)
WriteToFile(path string, data []byte) error
}
// GitService represents a service for managing Git
@@ -1339,7 +1341,7 @@ type (
const (
// APIVersion is the version number of the Portainer API
APIVersion = "2.11.0"
APIVersion = "2.11.1"
// DBVersion is the version number of the Portainer database
DBVersion = 35
// ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax

View File

@@ -14,6 +14,7 @@ export function configApp($urlRouterProvider, $httpProvider, localStorageService
tokenGetter: /* @ngInject */ function tokenGetter(LocalStorage) {
return LocalStorage.getJWT();
},
whiteListedDomains: ['localhost'],
});
$httpProvider.interceptors.push('jwtInterceptor');

4
app/global.d.ts vendored
View File

@@ -18,3 +18,7 @@ declare module 'axios-progress-bar' {
instance?: AxiosInstance
): void;
}
interface Window {
ddExtension: boolean;
}

View File

@@ -7,9 +7,16 @@
<meta name="author" content="<%= author %>" />
<base id="base" />
<script>
var path = window.location.pathname.replace(/^\/+|\/+$/g, '');
var basePath = path ? '/' + path + '/' : '/';
document.getElementById('base').href = basePath;
if (window.origin == 'file://') {
// we are loading the app from a local file as in docker extension
document.getElementById('base').href = 'http://localhost:9000/';
window.ddExtension = true;
} else {
var path = window.location.pathname.replace(/^\/+|\/+$/g, '');
var basePath = path ? '/' + path + '/' : '/';
document.getElementById('base').href = basePath;
}
</script>
<!-- HTML5 shim, for IE6-8 support of HTML5 elements -->
@@ -62,9 +69,12 @@
</div>
<!-- Main Content -->
<div id="view" ui-view="content" ng-if="!applicationState.loading"></div> </div
><!-- End Page Content --> </div
><!-- End Content Wrapper --> </div
><!-- End Page Wrapper -->
</body></html
>
<div id="view" ui-view="content" ng-if="!applicationState.loading"></div>
</div>
<!-- End Page Content -->
</div>
<!-- End Content Wrapper -->
</div>
<!-- End Page Wrapper -->
</body>
</html>

View File

@@ -93,11 +93,13 @@ export default class KubectlShellController {
const wsProtocol = this.$window.location.protocol === 'https:' ? 'wss://' : 'ws://';
const path = baseHref() + 'api/websocket/kubernetes-shell';
const base = path.startsWith('http') ? path.replace(/^https?:\/\//i, '') : window.location.host + path;
const queryParams = Object.entries(params)
.map(([k, v]) => `${k}=${v}`)
.join('&');
const url = `${wsProtocol}${window.location.host}${path}?${queryParams}`;
const url = `${wsProtocol}${base}?${queryParams}`;
Terminal.applyAddon(fit);
this.state.shell.socket = new WebSocket(url);
this.state.shell.term = new Terminal();

View File

@@ -2,7 +2,7 @@ export default class HeaderContentController {
/* @ngInject */
constructor(Authentication) {
this.Authentication = Authentication;
this.display = !window.ddExtension;
this.username = null;
}

View File

@@ -1,11 +1,11 @@
<div class="breadcrumb-links">
<div class="pull-left" ng-transclude></div>
<div class="pull-right" ng-if="$ctrl.username">
<div class="pull-right" ng-if="$ctrl.username && $ctrl.display">
<a ui-sref="portainer.account" style="margin-right: 5px">
<u><i class="fa fa-wrench" aria-hidden="true"></i> my account </u>
<u> <i class="fa fa-wrench" aria-hidden="true"></i> my account </u>
</a>
<a ui-sref="portainer.logout({performApiLogout: true})" class="text-danger" style="margin-right: 25px" data-cy="template-logoutButton">
<u><i class="fa fa-sign-out-alt" aria-hidden="true"></i> log out</u>
<u> <i class="fa fa-sign-out-alt" aria-hidden="true"></i> log out</u>
</a>
</div>
</div>

View File

@@ -15,7 +15,7 @@ export function HeaderContent({ children }: PropsWithChildren<unknown>) {
return (
<div className="breadcrumb-links">
<div className="pull-left">{children}</div>
{user && (
{user && !window.ddExtension && (
<div className={clsx('pull-right', styles.userLinks)}>
<Link to="portainer.account" className={styles.link}>
<i

View File

@@ -2,7 +2,7 @@ export default class HeaderTitle {
/* @ngInject */
constructor(Authentication) {
this.Authentication = Authentication;
this.display = !window.ddExtension;
this.username = null;
}

View File

@@ -1,5 +1,8 @@
<div class="page white-space-normal">
{{ $ctrl.titleText }}
<span class="header_title_content" ng-transclude></span>
<span class="pull-right user-box" ng-if="$ctrl.username"> <i class="fa fa-user-circle" aria-hidden="true"></i> {{ $ctrl.username }} </span>
<span class="pull-right user-box" ng-if="$ctrl.username && $ctrl.display">
<i class="fa fa-user-circle" aria-hidden="true"></i>
{{ $ctrl.username }}
</span>
</div>

View File

@@ -17,9 +17,10 @@ export function HeaderTitle({ title, children }: PropsWithChildren<Props>) {
<div className="page white-space-normal">
{title}
<span className="header_title_content">{children}</span>
{user && (
{user && !window.ddExtension && (
<span className="pull-right user-box">
<i className="fa fa-user-circle" aria-hidden="true" /> {user.Username}
<i className="fa fa-user-circle" aria-hidden="true" />
{user.Username}
</span>
)}
</div>

View File

@@ -31,9 +31,11 @@ angular.module('portainer.app').factory('EndpointStatusInterceptor', [
}
function responseErrorInterceptor(rejection) {
var url = rejection.config.url;
if ((rejection.status === 502 || rejection.status === 503 || rejection.status === -1) && canBeOffline(url) && !EndpointProvider.offlineMode()) {
EndpointProvider.setOfflineMode(true);
if (rejection.config) {
var url = rejection.config.url;
if ((rejection.status === 502 || rejection.status === 503 || rejection.status === -1) && canBeOffline(url) && !EndpointProvider.offlineMode()) {
EndpointProvider.setOfflineMode(true);
}
}
return $q.reject(rejection);
}

View File

@@ -1,5 +1,8 @@
import { clear as clearSessionStorage } from './session-storage';
const DEFAULT_USER = 'admin';
const DEFAULT_PASSWORD = 'Passw0rd;';
angular.module('portainer.app').factory('Authentication', [
'$async',
'$state',
@@ -28,12 +31,14 @@ angular.module('portainer.app').factory('Authentication', [
async function initAsync() {
try {
const jwt = LocalStorage.getJWT();
if (jwt) {
await setUser(jwt);
if (!jwt || jwtHelper.isTokenExpired(jwt)) {
return tryAutoLoginExtension();
}
return !!jwt;
await setUser(jwt);
return true;
} catch (error) {
return false;
console.log('Unable to initialize authentication service', error);
return tryAutoLoginExtension();
}
}
@@ -47,6 +52,7 @@ angular.module('portainer.app').factory('Authentication', [
EndpointProvider.clean();
LocalStorage.cleanAuthData();
LocalStorage.storeLoginStateUUID('');
tryAutoLoginExtension();
}
function logout(performApiLogout) {
@@ -59,7 +65,15 @@ angular.module('portainer.app').factory('Authentication', [
async function OAuthLoginAsync(code) {
const response = await OAuth.validate({ code: code }).$promise;
await setUser(response.jwt);
const jwt = setJWTFromResponse(response);
await setUser(jwt);
}
function setJWTFromResponse(response) {
const jwt = response.jwt;
LocalStorage.storeJWT(jwt);
return response.jwt;
}
function OAuthLogin(code) {
@@ -68,7 +82,8 @@ angular.module('portainer.app').factory('Authentication', [
async function loginAsync(username, password) {
const response = await Auth.login({ username: username, password: password }).$promise;
await setUser(response.jwt);
const jwt = setJWTFromResponse(response);
await setUser(jwt);
}
function login(username, password) {
@@ -77,7 +92,7 @@ angular.module('portainer.app').factory('Authentication', [
function isAuthenticated() {
var jwt = LocalStorage.getJWT();
return jwt && !jwtHelper.isTokenExpired(jwt);
return !!jwt && !jwtHelper.isTokenExpired(jwt);
}
function getUserDetails() {
@@ -96,7 +111,6 @@ angular.module('portainer.app').factory('Authentication', [
}
async function setUser(jwt) {
LocalStorage.storeJWT(jwt);
var tokenPayload = jwtHelper.decodeToken(jwt);
user.username = tokenPayload.username;
user.ID = tokenPayload.id;
@@ -104,11 +118,16 @@ angular.module('portainer.app').factory('Authentication', [
await setUserTheme();
}
function isAdmin() {
if (user.role === 1) {
return true;
function tryAutoLoginExtension() {
if (!window.ddExtension) {
return false;
}
return false;
return login(DEFAULT_USER, DEFAULT_PASSWORD);
}
function isAdmin() {
return !!user && user.role === 1;
}
return service;

View File

@@ -19,7 +19,7 @@ angular.module('portainer.app').controller('MainController', [
$scope.$watch($scope.getWidth, function (newValue) {
if (newValue >= mobileView) {
const toggleValue = LocalStorage.getToolbarToggle();
$scope.toggle = typeof toggleValue === 'boolean' ? toggleValue : true;
$scope.toggle = typeof toggleValue === 'boolean' ? toggleValue : !window.ddExtension;
} else {
$scope.toggle = false;
}

View File

@@ -21,7 +21,10 @@
<div class="form-group">
<div class="col-sm-12">
<label for="toggle_logo" class="control-label text-left"> Use custom logo </label>
<label class="switch" style="margin-left: 20px"> <input type="checkbox" name="toggle_logo" ng-model="formValues.customLogo" /><i></i> </label>
<label class="switch" style="margin-left: 20px">
<input type="checkbox" name="toggle_logo" ng-model="formValues.customLogo" />
<i></i>
</label>
</div>
</div>
<div ng-if="formValues.customLogo">
@@ -40,7 +43,10 @@
<div class="form-group">
<div class="col-sm-12">
<label for="toggle_enableTelemetry" class="control-label text-left"> Allow the collection of anonymous statistics </label>
<label class="switch" style="margin-left: 20px"> <input type="checkbox" name="toggle_enableTelemetry" ng-model="formValues.enableTelemetry" /><i></i> </label>
<label class="switch" style="margin-left: 20px">
<input type="checkbox" name="toggle_enableTelemetry" ng-model="formValues.enableTelemetry" />
<i></i>
</label>
</div>
<div class="col-sm-12 text-muted small" style="margin-top: 10px">
You can find more information about this in our
@@ -128,7 +134,7 @@
</div>
</div>
<ssl-certificate-settings></ssl-certificate-settings>
<ssl-certificate-settings ng-show="$ctrl.showHTTPS"></ssl-certificate-settings>
<div class="row">
<div class="col-sm-12">
@@ -149,7 +155,7 @@
<input type="text" class="form-control" id="header_value" ng-model="formValues.labelValue" placeholder="e.g. bar" />
</div>
<div class="col-sm-12 col-md-2 margin-sm-top">
<button type="submit" class="btn btn-primary btn-sm" ng-disabled="!formValues.labelName"><i class="fa fa-plus space-right" aria-hidden="true"></i>Add filter</button>
<button type="submit" class="btn btn-primary btn-sm" ng-disabled="!formValues.labelName"> <i class="fa fa-plus space-right" aria-hidden="true"></i>Add filter</button>
</div>
</div>
<div class="form-group">
@@ -166,11 +172,11 @@
<tr ng-repeat="label in settings.BlackListedLabels">
<td>{{ label.name }}</td>
<td>{{ label.value }}</td>
<td
><button type="button" class="btn btn-danger btn-xs" ng-click="removeFilteredContainerLabel($index)"
><i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove</button
></td
>
<td>
<button type="button" class="btn btn-danger btn-xs" ng-click="removeFilteredContainerLabel($index)">
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove</button
>
</td>
</tr>
<tr ng-if="settings.BlackListedLabels.length === 0">
<td colspan="3" class="text-center text-muted">No filter available.</td>
@@ -208,8 +214,8 @@
checked="formValues.scheduleAutomaticBackups"
label-class="'col-sm-2'"
on-change="(onToggleAutoBackups)"
></por-switch-field
></div>
></por-switch-field>
</div>
</div>
<!-- !Schedule automatic backups -->
<!-- Cron rule -->
@@ -311,7 +317,8 @@
<label for="password_protect" class="col-sm-1 control-label text-left">Password protect</label>
<div class="col-sm-1">
<label class="switch" data-cy="settings-s3PasswordToggle">
<input type="checkbox" id="password_protect_s3" name="password_protect_s3" ng-model="formValues.passwordProtectS3" disabled /><i></i>
<input type="checkbox" id="password_protect_s3" name="password_protect_s3" ng-model="formValues.passwordProtectS3" disabled />
<i></i>
</label>
</div>
</div>
@@ -342,7 +349,7 @@
limited-feature-disabled
limited-feature-class="limited-be"
>
<span><i class="fa fa-upload" aria-hidden="true"></i> Export backup</span>
<span> <i class="fa fa-upload" aria-hidden="true"></i> Export backup</span>
</button>
</div>
</div>
@@ -370,7 +377,8 @@
<label for="password_protect" class="col-sm-1 control-label text-left">Password protect</label>
<div class="col-sm-1">
<label class="switch" data-cy="settings-passwordProtectLocal">
<input type="checkbox" id="password_protect" name="password_protect" ng-model="formValues.passwordProtect" /><i></i>
<input type="checkbox" id="password_protect" name="password_protect" ng-model="formValues.passwordProtect" />
<i></i>
</label>
</div>
</div>

View File

@@ -45,6 +45,7 @@ angular.module('portainer.app').controller('SettingsController', [
],
backupInProgress: false,
featureLimited: false,
showHTTPS: !window.ddExtension,
};
$scope.BACKUP_FORM_TYPES = { S3: 's3', FILE: 'file' };

View File

@@ -50,6 +50,7 @@
<sidebar-section ng-if="isAdmin || isTeamLeader" title="Settings">
<sidebar-menu
ng-show="display"
icon-class="fa-users fa-fw"
label="Users"
path="portainer.users"
@@ -87,7 +88,9 @@
is-sidebar-open="toggle"
children-paths="['portainer.settings.authentication', 'portainer.settings.edgeCompute']"
>
<sidebar-menu-item path="portainer.settings.authentication" class-name="sidebar-sublist" data-cy="portainerSidebar-authentication">Authentication</sidebar-menu-item>
<sidebar-menu-item path="portainer.settings.authentication" class-name="sidebar-sublist" data-cy="portainerSidebar-authentication" ng-show="display"
>Authentication</sidebar-menu-item
>
<sidebar-menu-item path="portainer.settings.edgeCompute" class-name="sidebar-sublist" data-cy="portainerSidebar-edge-compute">Edge Compute</sidebar-menu-item>
<div class="sidebar-sublist">

View File

@@ -3,6 +3,7 @@ angular.module('portainer.app').controller('SidebarController', SidebarControlle
function SidebarController($rootScope, $scope, $transitions, StateManager, Notifications, Authentication, UserService, EndpointProvider) {
$scope.applicationState = StateManager.getState();
$scope.endpointState = EndpointProvider.endpoint();
$scope.display = !window.ddExtension;
function checkPermissions(memberships) {
var isLeader = false;

View File

@@ -0,0 +1,32 @@
# Makefile for development purpose
.PHONY: all build
all: clean build-local install-local
ORG=portainer
VERSION=2.11.12
IMAGE_NAME=$(ORG)/portainer-docker-extension
TAGGED_IMAGE_NAME=$(IMAGE_NAME):$(VERSION)
clean:
-docker extension remove $(IMAGE_NAME)
-docker rmi $(PORTAINER_IMAGE_NAME):$(VERSION)
-docker rmi $(IMAGE_NAME)
build-local:
docker buildx build -f build/linux/Dockerfile --load --build-arg TAG=$(VERSION) --build-arg PORTAINER_IMAGE_NAME=$(IMAGE_NAME) --tag=$(IMAGE_NAME):local-development .
install-local:
docker extension install portainer/portainer-docker-extension:local-development
build-remote:
docker buildx build -f build/linux/Dockerfile --push --builder=buildx-multi-arch --platform=windows/amd64,linux/amd64,linux/arm64 --build-arg TAG=$(VERSION) --build-arg PORTAINER_IMAGE_NAME=$(IMAGE_NAME) --tag=$(TAGGED_IMAGE_NAME) .
install-remote:
docker extension install $(TAGGED_IMAGE_NAME)
multiarch:
docker buildx create --name=buildx-multi-arch --driver=docker-container --driver-opt=network=host
portainer:
yarn build

View File

@@ -0,0 +1,20 @@
version: '3'
services:
portainer:
image: ${DESKTOP_PLUGIN_IMAGE}
command: ["--admin-password", "$$$$2a$$$$10$$$$HX4qSZhtQcvKUNmAsPXuPe9POkM7gdaFPcSnRjokgb8EkMI.1gkSa"]
# command: ["--generate-password"]
restart: unless-stopped
security_opt:
- no-new-privileges:true
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- portainer_data:/data
ports:
- 127.0.0.1:8000:8000
- 127.0.0.1:9000:9000
- 127.0.0.1:9443:9443
volumes:
portainer_data:

View File

@@ -0,0 +1,17 @@
{
"name": "Portainer",
"provider": "Portainer.io",
"icon": "portainer.svg",
"vm": {
"composefile": "docker-compose.yml",
"exposes": { "socket": "docker.sock" }
},
"ui": {
"dashboard-tab": {
"title": "Portainer",
"root": "/public",
"src": "index.html",
"backend": { "tcp": "9000" }
}
}
}

View File

@@ -0,0 +1 @@
<svg height="2500" viewBox=".16 0 571.71 800" width="1788" xmlns="http://www.w3.org/2000/svg"><g fill="#13bef9"><path d="m190.83 175.88h-12.2v63.2h12.2zm52.47 0h-12.2v63.2h12.2zm71.69-120.61-12.5-21.68-208.67 120.61 12.5 21.68z"/><path d="m313.77 55.27 12.51-21.68 208.67 120.61-12.51 21.68z"/><path d="m571.87 176.18v-25.03h-571.71v25.03z"/><path d="m345.5 529.77v-370.99h25.02v389.01c-6.71-7.64-15.26-13.13-25.02-18.02zm-42.71-6.41v-523.36h25.02v526.41c-7.02-3.36-24.1-3.05-25.02-3.05zm-237.04 52.21c-30.51-22.59-50.64-58.62-50.64-99.54 0-21.68 5.79-43.05 16.47-61.68h213.55c10.98 18.63 16.48 40 16.48 61.68 0 18.93-2.44 36.64-10.07 52.52-16.17-15.57-39.97-22.29-64.07-22.29-42.71 0-79.32 26.56-88.77 66.26-3.36-.31-5.49-.61-8.85-.61-8.24.3-16.17 1.53-24.1 3.66z" fill-rule="evenodd"/><path d="m170.69 267.18h-64.67v65.03h64.67zm-72.91 0h-64.67v65.03h64.67zm0 72.36h-64.67v65.04h64.67zm72.91 0h-64.67v65.04h64.67zm72.61 0h-64.67v65.04h64.67zm0-107.17h-64.67v65.03h64.67z"/><path d="m109.37 585.34c8.85-37.55 42.71-65.65 82.98-65.65 25.94 0 49.12 11.61 64.99 29.93 13.72-9.47 30.2-14.96 48.2-14.96 46.98 0 85.11 38.16 85.11 85.19 0 9.77-1.52 18.93-4.57 27.78 10.37 14.05 16.78 31.76 16.78 50.69 0 47.02-38.14 85.19-85.12 85.19-20.75 0-39.66-7.33-54.3-19.54-15.56 21.68-40.88 36.03-69.56 36.03-32.95 0-61.63-18.93-75.96-46.41-5.8 1.22-11.6 1.83-17.7 1.83-46.98 0-85.42-38.17-85.42-85.19s38.14-85.19 85.42-85.19c3.05-.31 6.1-.31 9.15.3z" fill-rule="evenodd"/></g></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
build/docker-extension/pwd Executable file

Binary file not shown.

View File

@@ -0,0 +1,15 @@
package main
import (
"fmt"
"os"
)
func main() {
// Read file '.pwd'
pwd, err := os.ReadFile(".pwd")
if err != nil {
panic(err)
}
fmt.Print(pwd)
}

View File

@@ -1,6 +1,13 @@
FROM portainer/base
LABEL org.opencontainers.image.title="Portainer" \
org.opencontainers.image.description="Rich container management experience using Portainer." \
org.opencontainers.image.vendor="Portainer.io" \
com.docker.desktop.extension.api.version=">= 0.2.0" \
com.docker.desktop.extension.icon=https://portainer-io-assets.sfo2.cdn.digitaloceanspaces.com/logos/portainer.png
COPY dist /
COPY build/docker-extension /
VOLUME /data
WORKDIR /

View File

@@ -41,6 +41,8 @@ module.exports = function (grunt) {
grunt.registerTask('build', ['build:server', 'build:client']);
grunt.registerTask('install:extension', ['shell:install_extension']);
grunt.registerTask('start:server', ['build:server', 'shell:run_container']);
grunt.registerTask('start:localserver', [`shell:build_binary:linux:${arch}`, 'shell:run_localserver']);
@@ -108,6 +110,7 @@ gruntConfig.clean = {
gruntConfig.shell = {
build_binary: { command: shell_build_binary },
build_binary_azuredevops: { command: shell_build_binary_azuredevops },
install_extension: { command: shell_install_extension },
download_docker_binary: { command: shell_download_docker_binary },
download_kompose_binary: { command: shell_download_kompose_binary },
download_kubectl_binary: { command: shell_download_kubectl_binary },
@@ -155,6 +158,10 @@ function shell_build_binary_azuredevops(platform, arch) {
return `build/build_binary_azuredevops.sh ${platform} ${arch};`;
}
function shell_install_extension() {
return `make all -f build/docker-extension/Makefile`;
}
function shell_run_container() {
const portainerData = '${PORTAINER_DATA:-/tmp/portainer}';
const portainerRoot = process.env.PORTAINER_PROJECT ? process.env.PORTAINER_PROJECT : process.env.PWD;

View File

@@ -32,6 +32,7 @@
"dev:client": "grunt clean:client && webpack-dev-server --config=./webpack/webpack.develop.js",
"dev:client:prod": "grunt clean:client && webpack-dev-server --config=./webpack/webpack.production.js",
"dev:nodl": "grunt clean:server && grunt clean:client && grunt build:server && grunt start:client",
"dev:extension": "grunt build && grunt install:extension",
"start:toolkit": "grunt start:toolkit",
"build:server:offline": "cd ./api/cmd/portainer && CGO_ENABLED=0 go build --installsuffix cgo --ldflags '-s' && mv -f portainer ../../../dist/portainer",
"clean:all": "grunt clean:all",