diff --git a/README.md b/README.md index 5bf9c1f..4c757a1 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,48 @@ -# Kubernetes Template Project +# minikube GUI -The Kubernetes Template Project is a template for starting new projects in the GitHub organizations owned by Kubernetes. All Kubernetes projects, at minimum, must have the following files: +## Usage -- a `README.md` outlining the project goals, sponsoring sig, and community contact information -- an `OWNERS` with the project leads listed as approvers ([docs on `OWNERS` files][owners]) -- a `CONTRIBUTING.md` outlining how to contribute to the project -- an unmodified copy of `code-of-conduct.md` from this repo, which outlines community behavior and the consequences of breaking the code -- a `LICENSE` which must be Apache 2.0 for code projects, or [Creative Commons 4.0] for documentation repositories, without any custom content -- a `SECURITY_CONTACTS` with the contact points for the Product Security Team - to reach out to for triaging and handling of incoming issues. They must agree to abide by the - [Embargo Policy](https://git.k8s.io/security/private-distributors-list.md#embargo-policy) - and will be removed and replaced if they violate that agreement. +### Prerequisites + +- minikube setup and available on your path, follow the [Getting Start doc](https://minikube.sigs.k8s.io/docs/start/) if not already done + +### Running + +#### From binary (end-user) + +View our documentation for downloading and running the GUI: [Setting Up minikube GUI](https://minikube.sigs.k8s.io/docs/tutorials/setup_minikube_gui/) + +#### From Qt Creator + +Open in [Qt Creator](https://doc.qt.io/qtcreator/) via: + +```shell +qtcreator minikube.pro +``` + +#### From Command Line + +```console +$ qmake +$ make +... +$ ./minikube +``` ## Community, discussion, contribution, and support -Learn how to engage with the Kubernetes community on the [community page](http://kubernetes.io/community/). +minikube GUI is a Kubernetes [#sig-cluster-lifecycle](https://github.com/kubernetes/community/tree/master/sig-cluster-lifecycle) project. + +* [**#minikube on Kubernetes Slack**](https://kubernetes.slack.com) - Live chat with minikube developers! +* [minikube-users mailing list](https://groups.google.com/g/minikube-users) +* [minikube-dev mailing list](https://groups.google.com/g/minikube-dev) -You can reach the maintainers of this project at: +* [Contributing](https://minikube.sigs.k8s.io/docs/contrib/) +* [Development Roadmap](https://minikube.sigs.k8s.io/docs/contrib/roadmap/) -- [Slack](https://slack.k8s.io/) -- [Mailing List](https://groups.google.com/a/kubernetes.io/g/dev) +Join our meetings: +* [Bi-weekly office hours, Mondays @ 11am PST](https://tinyurl.com/minikube-oh) +* [Triage Party](https://minikube.sigs.k8s.io/docs/contrib/triage/) ### Code of conduct diff --git a/advancedview.cpp b/advancedview.cpp new file mode 100644 index 0000000..0b60c7d --- /dev/null +++ b/advancedview.cpp @@ -0,0 +1,308 @@ +/* +Copyright 2022 The Kubernetes Authors All rights reserved. + +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. +*/ + +#include "advancedview.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +AdvancedView::AdvancedView(QIcon icon) +{ + m_icon = icon; + + advancedView = new QWidget(); + advancedView->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored); + + ClusterList clusters; + m_clusterModel = new ClusterModel(clusters); + + clusterListView = new QTableView(); + clusterListView->setModel(m_clusterModel); + clusterListView->setSelectionMode(QAbstractItemView::SingleSelection); + clusterListView->setSelectionBehavior(QAbstractItemView::SelectRows); + clusterListView->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch); + clusterListView->horizontalHeader()->setSectionResizeMode(1, QHeaderView::ResizeToContents); + clusterListView->horizontalHeader()->setSectionResizeMode(2, QHeaderView::ResizeToContents); + clusterListView->horizontalHeader()->setSectionResizeMode(3, QHeaderView::ResizeToContents); + clusterListView->horizontalHeader()->setSectionResizeMode(4, QHeaderView::ResizeToContents); + clusterListView->horizontalHeader()->setSectionResizeMode(5, QHeaderView::ResizeToContents); + clusterListView->horizontalHeader()->setSectionResizeMode(6, QHeaderView::ResizeToContents); + setSelectedClusterName("default"); + + startButton = new QPushButton(tr("Start")); + stopButton = new QPushButton(tr("Stop")); + pauseButton = new QPushButton(tr("Pause")); + deleteButton = new QPushButton(tr("Delete")); + refreshButton = new QPushButton(tr("Refresh")); + createButton = new QPushButton(tr("Create")); + dockerEnvButton = new QPushButton(tr("docker-env")); + sshButton = new QPushButton(tr("SSH")); + dashboardButton = new QPushButton(tr("Dashboard")); + basicButton = new QPushButton(tr("Basic View")); + + disableButtons(); + + QHBoxLayout *topButtonLayout = new QHBoxLayout; + topButtonLayout->addWidget(createButton); + topButtonLayout->addWidget(refreshButton); + topButtonLayout->addWidget(basicButton); + topButtonLayout->addSpacing(340); + + QHBoxLayout *bottomButtonLayout = new QHBoxLayout; + bottomButtonLayout->addWidget(startButton); + bottomButtonLayout->addWidget(stopButton); + bottomButtonLayout->addWidget(pauseButton); + bottomButtonLayout->addWidget(deleteButton); + bottomButtonLayout->addWidget(dockerEnvButton); + bottomButtonLayout->addWidget(sshButton); + bottomButtonLayout->addWidget(dashboardButton); + + QVBoxLayout *clusterLayout = new QVBoxLayout; + clusterLayout->addLayout(topButtonLayout); + clusterLayout->addWidget(clusterListView); + clusterLayout->addLayout(bottomButtonLayout); + advancedView->setLayout(clusterLayout); + + QFont *loadingFont = new QFont(); + loadingFont->setPointSize(30); + loading = new QLabel("Loading..."); + loading->setFont(*loadingFont); + loading->setParent(clusterListView); + loading->setHidden(true); + + connect(startButton, &QAbstractButton::clicked, this, &AdvancedView::start); + connect(stopButton, &QAbstractButton::clicked, this, &AdvancedView::stop); + connect(pauseButton, &QAbstractButton::clicked, this, &AdvancedView::pause); + connect(deleteButton, &QAbstractButton::clicked, this, &AdvancedView::delete_); + connect(refreshButton, &QAbstractButton::clicked, this, &AdvancedView::refresh); + connect(createButton, &QAbstractButton::clicked, this, &AdvancedView::askName); + connect(dockerEnvButton, &QAbstractButton::clicked, this, &AdvancedView::dockerEnv); + connect(sshButton, &QAbstractButton::clicked, this, &AdvancedView::ssh); + connect(dashboardButton, &QAbstractButton::clicked, this, &AdvancedView::dashboard); + connect(basicButton, &QAbstractButton::clicked, this, &AdvancedView::basic); +} + +static QString getPauseLabel(bool isPaused) +{ + if (isPaused) { + return "Unpause"; + } + return "Pause"; +} + +static QString getStartLabel(bool isRunning) +{ + if (isRunning) { + return "Restart"; + } + return "Start"; +} + +void AdvancedView::update(Cluster cluster) +{ + basicButton->setEnabled(true); + createButton->setEnabled(true); + refreshButton->setEnabled(true); + bool exists = !cluster.isEmpty(); + bool isRunning = cluster.status() == "Running"; + bool isPaused = cluster.status() == "Paused"; + startButton->setEnabled(exists); + stopButton->setEnabled(isRunning || isPaused); + pauseButton->setEnabled(isRunning || isPaused); + deleteButton->setEnabled(exists); + dashboardButton->setEnabled(isRunning); +#if __linux__ || __APPLE__ + dockerEnvButton->setEnabled(isRunning); + sshButton->setEnabled(exists); +#else + dockerEnvButton->setEnabled(false); + sshButton->setEnabled(false); +#endif + pauseButton->setText(getPauseLabel(isPaused)); + startButton->setText(getStartLabel(isRunning)); + QString startToolTip = ""; + if (isRunning) { + startToolTip = "Restart an already running minikube instance to pickup config changes."; + } + startButton->setToolTip(startToolTip); +} + +void AdvancedView::setSelectedClusterName(QString cluster) +{ + QAbstractItemModel *model = clusterListView->model(); + QModelIndex start = model->index(0, 0); + QModelIndexList index = model->match(start, Qt::DisplayRole, cluster); + if (index.size() == 0) { + return; + } + clusterListView->setCurrentIndex(index[0]); +} + +QString AdvancedView::selectedClusterName() +{ + QModelIndex index = clusterListView->currentIndex(); + QVariant variant = index.siblingAtColumn(0).data(Qt::DisplayRole); + if (variant.isNull()) { + return QString(); + } + return variant.toString(); +} + +void AdvancedView::updateClustersTable(ClusterList clusterList) +{ + QString cluster = selectedClusterName(); + m_clusterModel->setClusters(clusterList); + setSelectedClusterName(cluster); +} + +static int getCenter(int widgetSize, int parentSize) +{ + return parentSize / 2 - widgetSize / 2; +} + +void AdvancedView::showLoading() +{ + clusterListView->setEnabled(false); + loading->setHidden(false); + loading->raise(); + int width = getCenter(loading->width(), clusterListView->width()); + int height = getCenter(loading->height(), clusterListView->height()); + loading->move(width, height); +} + +void AdvancedView::hideLoading() +{ + loading->setHidden(true); + clusterListView->setEnabled(true); +} + +static QString profile = "minikube"; +static int cpus = 2; +static int memory = 2400; +static QString driver = ""; +static QString containerRuntime = ""; +static QString k8sVersion = ""; + +void AdvancedView::askName() +{ + QDialog dialog; + dialog.setWindowTitle(tr("Create minikube Cluster")); + dialog.setWindowIcon(m_icon); + dialog.setModal(true); + + QFormLayout form(&dialog); + QDialogButtonBox buttonBox(Qt::Horizontal, &dialog); + QLineEdit profileField(profile, &dialog); + form.addRow(new QLabel(tr("Profile")), &profileField); + buttonBox.addButton(QString(tr("Use Default Values")), QDialogButtonBox::AcceptRole); + connect(&buttonBox, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); + buttonBox.addButton(QString(tr("Set Custom Values")), QDialogButtonBox::RejectRole); + connect(&buttonBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); + form.addRow(&buttonBox); + + int code = dialog.exec(); + profile = profileField.text(); + if (code == QDialog::Accepted) { + QStringList args = { "-p", profile }; + emit createCluster(args); + } else if (code == QDialog::Rejected) { + askCustom(); + } +} + +void AdvancedView::askCustom() +{ + QDialog dialog; + dialog.setWindowTitle(tr("Set Cluster Values")); + dialog.setWindowIcon(m_icon); + dialog.setModal(true); + + QFormLayout form(&dialog); + QComboBox *driverComboBox = new QComboBox; + driverComboBox->addItems({ "docker", "virtualbox", "vmware", "podman" }); +#if __linux__ + driverComboBox->addItems({ "kvm2", "qemu" }); +#elif __APPLE__ + driverComboBox->addItems({ "hyperkit", "qemu", "parallels" }); +#else + driverComboBox->addItem("hyperv"); +#endif + form.addRow(new QLabel(tr("Driver")), driverComboBox); + QComboBox *containerRuntimeComboBox = new QComboBox; + containerRuntimeComboBox->addItems({ "docker", "containerd", "crio" }); + form.addRow(new QLabel(tr("Container Runtime")), containerRuntimeComboBox); + QComboBox *k8sVersionComboBox = new QComboBox; + k8sVersionComboBox->addItems({ "stable", "latest", "none" }); + form.addRow(new QLabel(tr("Kubernetes Version")), k8sVersionComboBox); + QLineEdit cpuField(QString::number(cpus), &dialog); + form.addRow(new QLabel(tr("CPUs")), &cpuField); + QLineEdit memoryField(QString::number(memory), &dialog); + form.addRow(new QLabel(tr("Memory")), &memoryField); + + QDialogButtonBox buttonBox(Qt::Horizontal, &dialog); + buttonBox.addButton(QString(tr("Create")), QDialogButtonBox::AcceptRole); + connect(&buttonBox, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); + buttonBox.addButton(QString(tr("Cancel")), QDialogButtonBox::RejectRole); + connect(&buttonBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); + form.addRow(&buttonBox); + + int code = dialog.exec(); + if (code == QDialog::Accepted) { + driver = driverComboBox->itemText(driverComboBox->currentIndex()); + containerRuntime = + containerRuntimeComboBox->itemText(containerRuntimeComboBox->currentIndex()); + k8sVersion = k8sVersionComboBox->itemText(k8sVersionComboBox->currentIndex()); + if (k8sVersion == "none") { + k8sVersion = "v0.0.0"; + } + cpus = cpuField.text().toInt(); + memory = memoryField.text().toInt(); + QStringList args = { "-p", + profile, + "--driver", + driver, + "--container-runtime", + containerRuntime, + "--kubernetes-version", + k8sVersion, + "--cpus", + QString::number(cpus), + "--memory", + QString::number(memory) }; + emit createCluster(args); + } +} + +void AdvancedView::disableButtons() +{ + startButton->setEnabled(false); + stopButton->setEnabled(false); + pauseButton->setEnabled(false); + deleteButton->setEnabled(false); + dockerEnvButton->setEnabled(false); + sshButton->setEnabled(false); + dashboardButton->setEnabled(false); + basicButton->setEnabled(false); + createButton->setEnabled(false); + refreshButton->setEnabled(false); +} diff --git a/advancedview.h b/advancedview.h new file mode 100644 index 0000000..15fafd9 --- /dev/null +++ b/advancedview.h @@ -0,0 +1,77 @@ +/* +Copyright 2022 The Kubernetes Authors All rights reserved. + +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. +*/ + +#ifndef ADVANCEDVIEW_H +#define ADVANCEDVIEW_H + +#include "cluster.h" + +#include +#include +#include +#include + +class AdvancedView : public QObject +{ + Q_OBJECT + +public: + explicit AdvancedView(QIcon icon); + QWidget *advancedView; + QTableView *clusterListView; + + QString selectedClusterName(); + void updateClustersTable(ClusterList clusters); + void showLoading(); + void hideLoading(); + void disableButtons(); + +public slots: + void update(Cluster cluster); + +signals: + void start(); + void stop(); + void pause(); + void delete_(); + void refresh(); + void dockerEnv(); + void ssh(); + void dashboard(); + void basic(); + void createCluster(QStringList args); + +private: + void setSelectedClusterName(QString cluster); + void askName(); + void askCustom(); + + QPushButton *startButton; + QPushButton *stopButton; + QPushButton *pauseButton; + QPushButton *deleteButton; + QPushButton *refreshButton; + QPushButton *dockerEnvButton; + QPushButton *sshButton; + QPushButton *dashboardButton; + QPushButton *basicButton; + QPushButton *createButton; + QLabel *loading; + ClusterModel *m_clusterModel; + QIcon m_icon; +}; + +#endif // ADVANCEDVIEW_H diff --git a/basicview.cpp b/basicview.cpp new file mode 100644 index 0000000..77a862c --- /dev/null +++ b/basicview.cpp @@ -0,0 +1,119 @@ +/* +Copyright 2022 The Kubernetes Authors All rights reserved. + +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. +*/ + +#include "basicview.h" + +#include + +BasicView::BasicView() +{ + basicView = new QWidget(); + + startButton = new QPushButton(tr("Start")); + stopButton = new QPushButton(tr("Stop")); + pauseButton = new QPushButton(tr("Pause")); + deleteButton = new QPushButton(tr("Delete")); + refreshButton = new QPushButton(tr("Refresh")); + dockerEnvButton = new QPushButton(tr("docker-env")); + dockerEnvButton->setToolTip( + "Opens a terminal where the docker-cli points to docker engine inside " + "minikube\n(Useful for building docker images directly inside minikube)"); + sshButton = new QPushButton(tr("SSH")); + dashboardButton = new QPushButton(tr("Dashboard")); + advancedButton = new QPushButton(tr("Advanced View")); + + disableButtons(); + + QVBoxLayout *buttonLayout = new QVBoxLayout; + basicView->setLayout(buttonLayout); + buttonLayout->addWidget(startButton); + buttonLayout->addWidget(stopButton); + buttonLayout->addWidget(pauseButton); + buttonLayout->addWidget(deleteButton); + buttonLayout->addWidget(refreshButton); + buttonLayout->addWidget(dockerEnvButton); + buttonLayout->addWidget(sshButton); + buttonLayout->addWidget(dashboardButton); + buttonLayout->addWidget(advancedButton); + basicView->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored); + + connect(startButton, &QPushButton::clicked, this, &BasicView::start); + connect(stopButton, &QAbstractButton::clicked, this, &BasicView::stop); + connect(pauseButton, &QAbstractButton::clicked, this, &BasicView::pause); + connect(deleteButton, &QAbstractButton::clicked, this, &BasicView::delete_); + connect(refreshButton, &QAbstractButton::clicked, this, &BasicView::refresh); + connect(dockerEnvButton, &QAbstractButton::clicked, this, &BasicView::dockerEnv); + connect(sshButton, &QAbstractButton::clicked, this, &BasicView::ssh); + connect(dashboardButton, &QAbstractButton::clicked, this, &BasicView::dashboard); + connect(advancedButton, &QAbstractButton::clicked, this, &BasicView::advanced); +} + +static QString getPauseLabel(bool isPaused) +{ + if (isPaused) { + return "Unpause"; + } + return "Pause"; +} + +static QString getStartLabel(bool isRunning) +{ + if (isRunning) { + return "Restart"; + } + return "Start"; +} + +void BasicView::update(Cluster cluster) +{ + startButton->setEnabled(true); + advancedButton->setEnabled(true); + refreshButton->setEnabled(true); + bool exists = !cluster.isEmpty(); + bool isRunning = cluster.status() == "Running"; + bool isPaused = cluster.status() == "Paused"; + stopButton->setEnabled(isRunning || isPaused); + pauseButton->setEnabled(isRunning || isPaused); + deleteButton->setEnabled(exists); + dashboardButton->setEnabled(isRunning); +#if __linux__ || __APPLE__ + dockerEnvButton->setEnabled(isRunning); + sshButton->setEnabled(exists); +#else + dockerEnvButton->setEnabled(false); + sshButton->setEnabled(false); +#endif + pauseButton->setText(getPauseLabel(isPaused)); + startButton->setText(getStartLabel(isRunning)); + QString startToolTip = ""; + if (isRunning) { + startToolTip = "Restart an already running minikube instance to pickup config changes."; + } + startButton->setToolTip(startToolTip); +} + +void BasicView::disableButtons() +{ + startButton->setEnabled(false); + stopButton->setEnabled(false); + deleteButton->setEnabled(false); + pauseButton->setEnabled(false); + dockerEnvButton->setEnabled(false); + sshButton->setEnabled(false); + dashboardButton->setEnabled(false); + advancedButton->setEnabled(false); + refreshButton->setEnabled(false); +} diff --git a/basicview.h b/basicview.h new file mode 100644 index 0000000..b865d73 --- /dev/null +++ b/basicview.h @@ -0,0 +1,58 @@ +/* +Copyright 2022 The Kubernetes Authors All rights reserved. + +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. +*/ + +#ifndef BASICVIEW_H +#define BASICVIEW_H + +#include "cluster.h" + +#include +#include + +class BasicView : public QObject +{ + Q_OBJECT + +public: + explicit BasicView(); + QWidget *basicView; + void update(Cluster cluster); + void disableButtons(); + +signals: + void start(); + void stop(); + void pause(); + void delete_(); + void refresh(); + void dockerEnv(); + void ssh(); + void dashboard(); + void advanced(); + +private: + QPushButton *startButton; + QPushButton *stopButton; + QPushButton *pauseButton; + QPushButton *deleteButton; + QPushButton *refreshButton; + QPushButton *dockerEnvButton; + QPushButton *sshButton; + QPushButton *dashboardButton; + QPushButton *advancedButton; +}; + +#endif // BASICVIEW_H diff --git a/cluster.cpp b/cluster.cpp new file mode 100644 index 0000000..b0bbbb5 --- /dev/null +++ b/cluster.cpp @@ -0,0 +1,114 @@ +/* +Copyright 2022 The Kubernetes Authors All rights reserved. + +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. +*/ + +#include "cluster.h" + +#include + +void ClusterModel::setClusters(const ClusterList &clusters) +{ + beginResetModel(); + clusterList = clusters; + endResetModel(); +} + +int ClusterModel::rowCount(const QModelIndex &) const +{ + return clusterList.count(); +} + +int ClusterModel::columnCount(const QModelIndex &) const +{ + return 7; +} + +static QStringList binaryAbbrs = { "B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB" }; + +QVariant ClusterModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + if (index.row() >= clusterList.size()) + return QVariant(); + if (index.column() >= 7) + return QVariant(); + + if (role == Qt::TextAlignmentRole) { + switch (index.column()) { + case 0: + return QVariant(Qt::AlignLeft | Qt::AlignVCenter); + case 1: + return QVariant(Qt::AlignRight | Qt::AlignVCenter); + case 2: + // fall-through + case 3: + // fall-through + case 4: + // fall-through + case 5: + // fall-through + case 6: + return QVariant(Qt::AlignHCenter | Qt::AlignVCenter); + } + } + if (role == Qt::DisplayRole) { + Cluster cluster = clusterList.at(index.row()); + switch (index.column()) { + case 0: + return cluster.name(); + case 1: + return cluster.status(); + case 2: + return cluster.driver(); + case 3: + return cluster.containerRuntime(); + case 4: + return cluster.k8sVersion(); + case 5: + return QString::number(cluster.cpus()); + case 6: + return QString::number(cluster.memory()); + } + } + return QVariant(); +} + +QVariant ClusterModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (role != Qt::DisplayRole) + return QVariant(); + + if (orientation == Qt::Horizontal) { + switch (section) { + case 0: + return tr("Name"); + case 1: + return tr("Status"); + case 2: + return tr("Driver"); + case 3: + return tr("Container Runtime"); + case 4: + return tr("Kubernetes Version"); + case 5: + return tr("CPUs"); + case 6: + return tr("Memory (MB)"); + } + } + return QVariant(); // QStringLiteral("Row %1").arg(section); +} diff --git a/cluster.h b/cluster.h new file mode 100644 index 0000000..23c06e2 --- /dev/null +++ b/cluster.h @@ -0,0 +1,89 @@ +/* +Copyright 2022 The Kubernetes Authors All rights reserved. + +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. +*/ + +#ifndef CLUSTER_H +#define CLUSTER_H + +#include +#include +#include +#include + +class Cluster +{ +public: + Cluster() : Cluster("") { } + Cluster(const QString &name) + : m_name(name), + m_status(""), + m_driver(""), + m_container_runtime(""), + m_k8s_version(""), + m_cpus(0), + m_memory(0) + { + } + + QString name() const { return m_name; } + QString status() const { return m_status; } + void setStatus(QString status) { m_status = status; } + QString driver() const { return m_driver; } + void setDriver(QString driver) { m_driver = driver; } + QString containerRuntime() const { return m_container_runtime; } + void setContainerRuntime(QString containerRuntime) { m_container_runtime = containerRuntime; } + QString k8sVersion() const { return m_k8s_version; } + void setK8sVersion(QString k8sVersion) { m_k8s_version = k8sVersion; } + int cpus() const { return m_cpus; } + void setCpus(int cpus) { m_cpus = cpus; } + int memory() const { return m_memory; } + void setMemory(int memory) { m_memory = memory; } + bool isEmpty() { return m_name.isEmpty(); } + +private: + QString m_name; + QString m_status; + QString m_driver; + QString m_container_runtime; + QString m_k8s_version; + int m_cpus; + int m_memory; +}; + +typedef QList ClusterList; +typedef QHash ClusterHash; + +class ClusterModel : public QAbstractListModel +{ + Q_OBJECT + +public: + ClusterModel(const ClusterList &clusters, QObject *parent = nullptr) + : QAbstractListModel(parent), clusterList(clusters) + { + } + + void setClusters(const ClusterList &clusters); + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + int columnCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role) const override; + QVariant headerData(int section, Qt::Orientation orientation, + int role = Qt::DisplayRole) const override; + +private: + ClusterList clusterList; +}; + +#endif // CLUSTER_H diff --git a/commandrunner.cpp b/commandrunner.cpp new file mode 100644 index 0000000..f2c41c7 --- /dev/null +++ b/commandrunner.cpp @@ -0,0 +1,257 @@ +/* +Copyright 2022 The Kubernetes Authors All rights reserved. + +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. +*/ + +#include "commandrunner.h" + +#include +#include +#include +#include +#include +#include + +CommandRunner::CommandRunner(QDialog *parent, Logger *logger) +{ + m_env = QProcessEnvironment::systemEnvironment(); + m_parent = parent; + m_logger = logger; + minikubePath(); +#if __APPLE__ + setMinikubePath(); +#endif +} + +void CommandRunner::executeCommand(QString program, QStringList args) +{ + QProcess *process = new QProcess(this); + process->setProcessEnvironment(m_env); + process->start(program, args); + process->waitForFinished(-1); + if (process->exitCode() == 0) { + return; + } + QString out = process->readAllStandardOutput(); + QString err = process->readAllStandardError(); + QString log = QString("The following command failed:\n%1 %2\n\nStdout:\n%3\n\nStderr:\n%4\n\n") + .arg(program, args.join(" "), out, err); + m_logger->log(log); + delete process; +} + +void CommandRunner::executeMinikubeCommand(QStringList args) +{ + m_isRunning = true; + m_output = ""; + QStringList userArgs = { "--user", "minikube-gui" }; + args << userArgs; + m_process = new QProcess(m_parent); + connect(m_process, QOverload::of(&QProcess::finished), this, + &CommandRunner::executionCompleted); + connect(m_process, &QProcess::readyReadStandardError, this, &CommandRunner::errorReady); + connect(m_process, &QProcess::readyReadStandardOutput, this, &CommandRunner::outputReady); + m_process->setProcessEnvironment(m_env); + m_process->start(m_minikubePath, args); + emit CommandRunner::startingExecution(); +} + +void CommandRunner::startMinikube(QStringList args) +{ + m_command = "start"; + QStringList baseArgs = { "start", "-o", "json" }; + baseArgs << args; + m_args = baseArgs; + executeMinikubeCommand(baseArgs); + emit startCommandStarting(); +} + +void CommandRunner::stopMinikube(QStringList args) +{ + QStringList baseArgs = { "stop" }; + baseArgs << args; + executeMinikubeCommand(baseArgs); +} + +void CommandRunner::pauseMinikube(QStringList args) +{ + QStringList baseArgs = { "pause" }; + baseArgs << args; + executeMinikubeCommand(baseArgs); +} + +void CommandRunner::unpauseMinikube(QStringList args) +{ + QStringList baseArgs = { "unpause" }; + baseArgs << args; + executeMinikubeCommand(baseArgs); +} + +void CommandRunner::deleteMinikube(QStringList args) +{ + m_command = "delete"; + QStringList baseArgs = { "delete" }; + baseArgs << args; + executeMinikubeCommand(baseArgs); +} + +void CommandRunner::stopCommand() +{ + m_process->terminate(); +} + +static Cluster createClusterObject(QJsonObject obj) +{ + QString name; + if (obj.contains("Name")) { + name = obj["Name"].toString(); + } + Cluster cluster(name); + if (obj.contains("Status")) { + QString status = obj["Status"].toString(); + cluster.setStatus(status); + } + if (!obj.contains("Config")) { + return cluster; + } + QJsonObject config = obj["Config"].toObject(); + if (config.contains("CPUs")) { + int cpus = config["CPUs"].toInt(); + cluster.setCpus(cpus); + } + if (config.contains("Memory")) { + int memory = config["Memory"].toInt(); + cluster.setMemory(memory); + } + if (config.contains("Driver")) { + QString driver = config["Driver"].toString(); + cluster.setDriver(driver); + } + if (!config.contains("KubernetesConfig")) { + return cluster; + } + QJsonObject k8sConfig = config["KubernetesConfig"].toObject(); + if (k8sConfig.contains("ContainerRuntime")) { + QString containerRuntime = k8sConfig["ContainerRuntime"].toString(); + cluster.setContainerRuntime(containerRuntime); + } + if (k8sConfig.contains("KubernetesVersion")) { + QString k8sVersion = k8sConfig["KubernetesVersion"].toString(); + cluster.setK8sVersion(k8sVersion); + } + return cluster; +} + +static ClusterList jsonToClusterList(QString text) +{ + ClusterList clusters; + QStringList lines; +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + lines = text.split("\n", Qt::SkipEmptyParts); +#else + lines = text.split("\n", QString::SkipEmptyParts); +#endif + for (int i = 0; i < lines.size(); i++) { + QString line = lines.at(i); + QJsonParseError error; + QJsonDocument json = QJsonDocument::fromJson(line.toUtf8(), &error); + if (json.isNull()) { + qDebug() << error.errorString(); + continue; + } + if (!json.isObject()) { + continue; + } + QJsonObject par = json.object(); + QJsonArray valid = par["valid"].toArray(); + QJsonArray invalid = par["invalid"].toArray(); + for (int i = 0; i < valid.size(); i++) { + QJsonObject obj = valid[i].toObject(); + Cluster cluster = createClusterObject(obj); + clusters << cluster; + } + for (int i = 0; i < invalid.size(); i++) { + QJsonObject obj = invalid[i].toObject(); + Cluster cluster = createClusterObject(obj); + cluster.setStatus("Invalid"); + clusters << cluster; + } + } + return clusters; +} + +void CommandRunner::requestClusters() +{ + m_command = "cluster"; + QStringList args = { "profile", "list", "-o", "json" }; + executeMinikubeCommand(args); +} + +void CommandRunner::executionCompleted() +{ + m_isRunning = false; + QString cmd = m_command; + m_command = ""; + QString output = m_output; + int exitCode = m_process->exitCode(); + delete m_process; + if (cmd != "cluster") { + emit executionEnded(); + } + if (cmd == "start" && exitCode != 0) { + emit error(m_args, output); + } + if (cmd == "cluster") { + ClusterList clusterList = jsonToClusterList(output); + emit updatedClusters(clusterList); + } +} + +void CommandRunner::errorReady() +{ + QString text = m_process->readAllStandardError(); + m_output.append(text); + emit output(text); +} + +void CommandRunner::outputReady() +{ + QString text = m_process->readAllStandardOutput(); + m_output.append(text); + emit output(text); +} + +#if __APPLE__ +void CommandRunner::setMinikubePath() +{ + m_env = QProcessEnvironment::systemEnvironment(); + QString path = m_env.value("PATH") + ":/usr/local/bin"; + m_env.insert("PATH", path); +} +#endif + +void CommandRunner::minikubePath() +{ + m_minikubePath = QStandardPaths::findExecutable("minikube"); + if (!m_minikubePath.isEmpty()) { + return; + } + QStringList path = { "/usr/local/bin" }; + m_minikubePath = QStandardPaths::findExecutable("minikube", path); +} + +bool CommandRunner::isRunning() +{ + return m_isRunning; +} diff --git a/commandrunner.h b/commandrunner.h new file mode 100644 index 0000000..9d7bd77 --- /dev/null +++ b/commandrunner.h @@ -0,0 +1,79 @@ +/* +Copyright 2022 The Kubernetes Authors All rights reserved. + +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. +*/ + +#ifndef COMMANDRUNNER_H +#define COMMANDRUNNER_H + +#include "cluster.h" +#include "logger.h" + +#include +#include +#include +#include +#include +#include +#include + +class CommandRunner : public QObject +{ + Q_OBJECT + +public: + CommandRunner(QDialog *parent, Logger *logger); + + void executeCommand(QString program, QStringList args); + void startMinikube(QStringList args); + void stopMinikube(QStringList args); + void pauseMinikube(QStringList args); + void unpauseMinikube(QStringList args); + void deleteMinikube(QStringList args); + void stopCommand(); + void requestClusters(); + bool isRunning(); + +signals: + void startingExecution(); + void executionEnded(); + void output(QString text); + void error(QStringList args, QString text); + void updatedClusters(ClusterList clusterList); + void startCommandStarting(); + +private slots: + void executionCompleted(); + void outputReady(); + void errorReady(); + +private: + void executeMinikubeCommand(QStringList args); + void minikubePath(); +#if __APPLE__ + void setMinikubePath(); +#endif + + QProcess *m_process; + QProcessEnvironment m_env; + QString m_output; + QString m_minikubePath; + QString m_command; + QDialog *m_parent; + Logger *m_logger; + QStringList m_args; + bool m_isRunning; +}; + +#endif // COMMANDRUNNER_H diff --git a/errormessage.cpp b/errormessage.cpp new file mode 100644 index 0000000..a9684ec --- /dev/null +++ b/errormessage.cpp @@ -0,0 +1,80 @@ +/* +Copyright 2022 The Kubernetes Authors All rights reserved. + +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. +*/ + +#include "errormessage.h" + +#include +#include +#include +#include +#include +#include + +ErrorMessage::ErrorMessage(QDialog *parent, QIcon icon) +{ + m_parent = parent; + m_icon = icon; +} + +void ErrorMessage::error(QString errorCode, QString advice, QString message, QString url, + QString issues) +{ + + m_dialog = new QDialog(m_parent); + m_dialog->setWindowTitle(tr("minikube start failed")); + m_dialog->setWindowIcon(m_icon); + m_dialog->setFixedWidth(600); + m_dialog->setModal(true); + QFormLayout form(m_dialog); + createLabel("Error Code", errorCode, &form, false); + createLabel("Advice", advice, &form, false); + QTextEdit *errorMessage = new QTextEdit(); + errorMessage->setText(message); + errorMessage->setWordWrapMode(QTextOption::WrapAnywhere); + int pointSize = errorMessage->font().pointSize(); + errorMessage->setFont(QFont("Courier", pointSize)); + errorMessage->setAutoFillBackground(true); + errorMessage->setReadOnly(true); + form.addRow(errorMessage); + createLabel("Link to documentation", url, &form, true); + createLabel("Link to related issue", issues, &form, true); + QLabel *fileLabel = new QLabel(); + fileLabel->setOpenExternalLinks(true); + fileLabel->setWordWrap(true); + QString logFile = QDir::homePath() + "/.minikube/logs/lastStart.txt"; + fileLabel->setText("View log file"); + form.addRow(fileLabel); + QDialogButtonBox buttonBox(Qt::Horizontal, m_dialog); + buttonBox.addButton(QString(tr("OK")), QDialogButtonBox::AcceptRole); + connect(&buttonBox, &QDialogButtonBox::accepted, m_dialog, &QDialog::accept); + form.addRow(&buttonBox); + m_dialog->exec(); +} + +QLabel *ErrorMessage::createLabel(QString title, QString text, QFormLayout *form, bool isLink) +{ + QLabel *label = new QLabel(); + if (!text.isEmpty()) { + form->addRow(label); + } + if (isLink) { + label->setOpenExternalLinks(true); + text = "" + text + ""; + } + label->setWordWrap(true); + label->setText(title + ": " + text); + return label; +} diff --git a/errormessage.h b/errormessage.h new file mode 100644 index 0000000..dea594b --- /dev/null +++ b/errormessage.h @@ -0,0 +1,43 @@ +/* +Copyright 2022 The Kubernetes Authors All rights reserved. + +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. +*/ + +#ifndef ERRORMESSAGE_H +#define ERRORMESSAGE_H + +#include +#include +#include +#include +#include + +class ErrorMessage : public QObject +{ + Q_OBJECT + +public: + explicit ErrorMessage(QDialog *parent, QIcon icon); + + void error(QString errorCode, QString advice, QString errorMessage, QString url, + QString issues); + QLabel *createLabel(QString title, QString text, QFormLayout *form, bool isLink); + +private: + QDialog *m_dialog; + QIcon m_icon; + QDialog *m_parent; +}; + +#endif // ERRORMESSAGE_H diff --git a/hyperkit.cpp b/hyperkit.cpp new file mode 100644 index 0000000..0b83abd --- /dev/null +++ b/hyperkit.cpp @@ -0,0 +1,73 @@ +/* +Copyright 2022 The Kubernetes Authors All rights reserved. + +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. +*/ + +#include "hyperkit.h" + +#include +#include + +HyperKit::HyperKit(QIcon icon) +{ + m_icon = icon; +} + +#if __APPLE__ +bool HyperKit::hyperkitPermissionFix(QStringList args, QString text) +{ + if (!text.contains("docker-machine-driver-hyperkit needs to run with elevated permissions")) { + return false; + } + if (!showHyperKitMessage()) { + return false; + } + + hyperkitPermission(); + emit rerun(args); + return true; +} + +void HyperKit::hyperkitPermission() +{ + QString command = "sudo chown root:wheel ~/.minikube/bin/docker-machine-driver-hyperkit && " + "sudo chmod u+s ~/.minikube/bin/docker-machine-driver-hyperkit && exit"; + QStringList arguments = { "-e", "tell app \"Terminal\"", + "-e", "set w to do script \"" + command + "\"", + "-e", "activate", + "-e", "repeat", + "-e", "delay 0.1", + "-e", "if not busy of w then exit repeat", + "-e", "end repeat", + "-e", "end tell" }; + QProcess *process = new QProcess(); + process->start("/usr/bin/osascript", arguments); + process->waitForFinished(-1); +} + +bool HyperKit::showHyperKitMessage() +{ + QMessageBox msgBox; + msgBox.setWindowTitle("HyperKit Permissions Required"); + msgBox.setWindowIcon(m_icon); + msgBox.setModal(true); + msgBox.setText("The HyperKit driver requires a one-time sudo permission.\n\nIf you'd like " + "to proceed, press OK and then enter your password into the terminal prompt, " + "the start will resume after."); + msgBox.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel); + msgBox.setDefaultButton(QMessageBox::Ok); + int code = msgBox.exec(); + return code == QMessageBox::Ok; +} +#endif diff --git a/hyperkit.h b/hyperkit.h new file mode 100644 index 0000000..800c30d --- /dev/null +++ b/hyperkit.h @@ -0,0 +1,41 @@ +/* +Copyright 2022 The Kubernetes Authors All rights reserved. + +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. +*/ + +#ifndef HYPERKIT_H +#define HYPERKIT_H + +#include +#include +#include + +class HyperKit : public QObject +{ + Q_OBJECT + +public: + explicit HyperKit(QIcon icon); + bool hyperkitPermissionFix(QStringList args, QString text); + +signals: + void rerun(QStringList args); + +private: + void hyperkitPermission(); + bool showHyperKitMessage(); + QIcon m_icon; +}; + +#endif // HYPERKIT_H diff --git a/images/minikube.icns b/images/minikube.icns new file mode 100644 index 0000000..392d780 Binary files /dev/null and b/images/minikube.icns differ diff --git a/images/minikube.png b/images/minikube.png new file mode 100644 index 0000000..9607014 Binary files /dev/null and b/images/minikube.png differ diff --git a/logger.cpp b/logger.cpp new file mode 100644 index 0000000..846a1a8 --- /dev/null +++ b/logger.cpp @@ -0,0 +1,42 @@ +/* +Copyright 2022 The Kubernetes Authors All rights reserved. + +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. +*/ + +#include "logger.h" + +#include +#include +#include +#include + +Logger::Logger() +{ + QDir dir = QDir(QDir::homePath() + "/.minikube-gui"); + if (!dir.exists()) { + dir.mkpath("."); + } + m_logPath = dir.filePath("logs.txt"); +} + +void Logger::log(QString message) +{ + QFile file(m_logPath); + if (!file.open(QIODevice::Append)) { + return; + } + QTextStream stream(&file); + stream << message << "\n"; + file.close(); +} diff --git a/logger.h b/logger.h new file mode 100644 index 0000000..c0ae79a --- /dev/null +++ b/logger.h @@ -0,0 +1,34 @@ +/* +Copyright 2022 The Kubernetes Authors All rights reserved. + +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. +*/ + +#ifndef LOGGER_H +#define LOGGER_H + +#include + +class Logger : public QObject +{ + Q_OBJECT + +public: + explicit Logger(); + void log(QString message); + +private: + QString m_logPath; +}; + +#endif // LOGGER_H diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..38acf6e --- /dev/null +++ b/main.cpp @@ -0,0 +1,58 @@ +/* +Copyright 2022 The Kubernetes Authors All rights reserved. + +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. +*/ + +#include + +#ifndef QT_NO_SYSTEMTRAYICON + +#include +#include "window.h" + +int main(int argc, char *argv[]) +{ + Q_INIT_RESOURCE(minikube); + + QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); + + QApplication app(argc, argv); + + if (!QSystemTrayIcon::isSystemTrayAvailable()) { + QMessageBox::critical(0, QObject::tr("minikube"), QObject::tr("No system tray detected.")); + return 1; + } + QApplication::setQuitOnLastWindowClosed(false); + + Window window; + window.show(); + return app.exec(); +} + +#else + +#include + +int main(int argc, char *argv[]) +{ + QApplication app(argc, argv); + + QLabel *label = new QLabel("System tray is not supported on this machine."); + label->setWordWrap(true); + label->show(); + + app.exec(); +} + +#endif diff --git a/minikube.pro b/minikube.pro new file mode 100644 index 0000000..8143572 --- /dev/null +++ b/minikube.pro @@ -0,0 +1,45 @@ +HEADERS = window.h \ + advancedview.h \ + basicview.h \ + cluster.h \ + commandrunner.h \ + errormessage.h \ + hyperkit.h \ + logger.h \ + operator.h \ + progresswindow.h \ + tray.h \ + updater.h +SOURCES = main.cpp \ + advancedview.cpp \ + basicview.cpp \ + cluster.cpp \ + commandrunner.cpp \ + errormessage.cpp \ + hyperkit.cpp \ + logger.cpp \ + operator.cpp \ + progresswindow.cpp \ + tray.cpp \ + updater.cpp \ + window.cpp +RESOURCES = minikube.qrc +ICON = images/minikube.icns + +QT += widgets network +requires(qtConfig(combobox)) + +DISTFILES += \ + LICENSE + +# Enabling qtermwidget requires GPL-v2 license +#CONFIG += gpl_licensed + +gpl_licensed { + win32: DEFINES += QT_NO_TERMWIDGET + + unix: CONFIG += link_pkgconfig + unix: PKGCONFIG += qtermwidget5 +} else { + DEFINES += QT_NO_TERMWIDGET +} diff --git a/minikube.qrc b/minikube.qrc new file mode 100644 index 0000000..6e9fe37 --- /dev/null +++ b/minikube.qrc @@ -0,0 +1,5 @@ + + + images/minikube.png + + diff --git a/operator.cpp b/operator.cpp new file mode 100644 index 0000000..8520a21 --- /dev/null +++ b/operator.cpp @@ -0,0 +1,434 @@ +/* +Copyright 2022 The Kubernetes Authors All rights reserved. + +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. +*/ + +#include "operator.h" + +#include +#include +#include +#include + +Operator::Operator(AdvancedView *advancedView, BasicView *basicView, CommandRunner *commandRunner, + ErrorMessage *errorMessage, ProgressWindow *progressWindow, Tray *tray, + HyperKit *hyperKit, Updater *updater, QStackedWidget *stackedWidget, + QDialog *parent) +{ + m_advancedView = advancedView; + m_basicView = basicView; + m_commandRunner = commandRunner; + m_errorMessage = errorMessage; + m_progressWindow = progressWindow; + m_tray = tray; + m_hyperKit = hyperKit; + m_updater = updater; + m_stackedWidget = stackedWidget; + m_parent = parent; + m_isBasicView = true; + dashboardProcess = NULL; + + connect(m_basicView, &BasicView::start, this, &Operator::startMinikube); + connect(m_basicView, &BasicView::stop, this, &Operator::stopMinikube); + connect(m_basicView, &BasicView::pause, this, &Operator::pauseOrUnpauseMinikube); + connect(m_basicView, &BasicView::delete_, this, &Operator::deleteMinikube); + connect(m_basicView, &BasicView::refresh, this, &Operator::updateClusters); + connect(m_basicView, &BasicView::dockerEnv, this, &Operator::dockerEnv); + connect(m_basicView, &BasicView::ssh, this, &Operator::sshConsole); + connect(m_basicView, &BasicView::dashboard, this, &Operator::dashboardBrowser); + connect(m_basicView, &BasicView::advanced, this, &Operator::toAdvancedView); + + connect(m_advancedView, &AdvancedView::start, this, &Operator::startMinikube); + connect(m_advancedView, &AdvancedView::stop, this, &Operator::stopMinikube); + connect(m_advancedView, &AdvancedView::pause, this, &Operator::pauseOrUnpauseMinikube); + connect(m_advancedView, &AdvancedView::delete_, this, &Operator::deleteMinikube); + connect(m_advancedView, &AdvancedView::refresh, this, &Operator::updateClusters); + connect(m_advancedView, &AdvancedView::dockerEnv, this, &Operator::dockerEnv); + connect(m_advancedView, &AdvancedView::ssh, this, &Operator::sshConsole); + connect(m_advancedView, &AdvancedView::dashboard, this, &Operator::dashboardBrowser); + connect(m_advancedView, &AdvancedView::basic, this, &Operator::toBasicView); + connect(m_advancedView, &AdvancedView::createCluster, this, &Operator::createCluster); + connect(m_advancedView->clusterListView, SIGNAL(clicked(QModelIndex)), this, + SLOT(updateButtons())); + + connect(m_commandRunner, &CommandRunner::startingExecution, this, &Operator::commandStarting); + connect(m_commandRunner, &CommandRunner::executionEnded, this, &Operator::commandEnding); + connect(m_commandRunner, &CommandRunner::output, this, &Operator::commandOutput); + connect(m_commandRunner, &CommandRunner::error, this, &Operator::commandError); + connect(m_commandRunner, &CommandRunner::updatedClusters, this, &Operator::clustersReceived); + connect(m_commandRunner, &CommandRunner::startCommandStarting, this, + &Operator::startCommandStarting); + + connect(m_progressWindow, &ProgressWindow::cancelled, this, &Operator::cancelCommand); + + connect(m_tray, &Tray::restoreWindow, this, &Operator::restoreWindow); + connect(m_tray, &Tray::hideWindow, this, &Operator::hideWindow); + connect(m_tray, &Tray::start, this, &Operator::startMinikube); + connect(m_tray, &Tray::stop, this, &Operator::stopMinikube); + connect(m_tray, &Tray::pauseOrUnpause, this, &Operator::pauseOrUnpauseMinikube); + + connect(m_hyperKit, &HyperKit::rerun, this, &Operator::createCluster); + + updateClusters(); +} + +QStringList Operator::getCurrentClusterFlags() +{ + return { "-p", selectedClusterName() }; +} + +void Operator::startMinikube() +{ + m_commandRunner->startMinikube(getCurrentClusterFlags()); +} + +void Operator::stopMinikube() +{ + m_commandRunner->stopMinikube(getCurrentClusterFlags()); +} + +void Operator::pauseOrUnpauseMinikube() +{ + Cluster cluster = selectedCluster(); + if (cluster.status() == "Paused") { + unpauseMinikube(); + return; + } + pauseMinikube(); +} + +void Operator::pauseMinikube() +{ + m_commandRunner->pauseMinikube(getCurrentClusterFlags()); +} + +void Operator::unpauseMinikube() +{ + m_commandRunner->unpauseMinikube(getCurrentClusterFlags()); +} + +void Operator::deleteMinikube() +{ + m_commandRunner->deleteMinikube(getCurrentClusterFlags()); +} + +void Operator::createCluster(QStringList args) +{ + m_commandRunner->startMinikube(args); +} + +void Operator::startCommandStarting() +{ + commandStarting(); + m_progressWindow->setText("Starting..."); + m_progressWindow->show(); +} + +void Operator::commandStarting() +{ + m_advancedView->showLoading(); + m_tray->disableActions(); + m_parent->setCursor(Qt::WaitCursor); + disableButtons(); +} + +void Operator::disableButtons() +{ + if (m_isBasicView) { + m_basicView->disableButtons(); + } else { + m_advancedView->disableButtons(); + } +} + +void Operator::commandEnding() +{ + m_progressWindow->done(); + updateClusters(); +} + +void Operator::toAdvancedView() +{ + m_isBasicView = false; + m_stackedWidget->setCurrentIndex(1); + m_parent->resize(670, 400); + updateButtons(); +} + +void Operator::toBasicView() +{ + m_isBasicView = true; + m_stackedWidget->setCurrentIndex(0); + m_parent->resize(200, 300); + updateButtons(); +} + +void Operator::updateClusters() +{ + m_commandRunner->requestClusters(); +} + +void Operator::clustersReceived(ClusterList clusterList) +{ + m_clusterList = clusterList; + m_advancedView->updateClustersTable(m_clusterList); + updateButtons(); + m_advancedView->hideLoading(); + m_parent->unsetCursor(); + m_updater->checkForUpdates(); +} + +void Operator::updateButtons() +{ + Cluster cluster = selectedCluster(); + if (m_isBasicView) { + m_basicView->update(cluster); + } else { + m_advancedView->update(cluster); + } + m_tray->updateTrayActions(cluster); + m_tray->updateStatus(cluster); +} + +void Operator::restoreWindow() +{ + bool wasVisible = m_parent->isVisible(); + m_parent->showNormal(); + m_parent->activateWindow(); + if (wasVisible) { + return; + } + if (m_commandRunner->isRunning()) + return; + updateClusters(); +} + +void Operator::hideWindow() +{ + m_parent->hide(); +} + +void Operator::commandOutput(QString text) +{ + QStringList lines; +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + lines = text.split("\n", Qt::SkipEmptyParts); +#else + lines = text.split("\n", QString::SkipEmptyParts); +#endif + for (int i = 0; i < lines.size(); i++) { + QJsonDocument json = QJsonDocument::fromJson(lines[i].toUtf8()); + QJsonObject object = json.object(); + QString type = object["type"].toString(); + if (type != "io.k8s.sigs.minikube.step") { + return; + } + QJsonObject data = object["data"].toObject(); + QString stringStep = data["currentstep"].toString(); + int currStep = stringStep.toInt(); + QString totalString = data["totalsteps"].toString(); + int totalSteps = totalString.toInt(); + QString message = data["message"].toString(); + m_progressWindow->setBarMaximum(totalSteps); + m_progressWindow->setBarValue(currStep); + m_progressWindow->setText(message); + } +} + +void Operator::commandError(QStringList args, QString text) +{ +#if __APPLE__ + if (m_hyperKit->hyperkitPermissionFix(args, text)) { + return; + } +#endif + QStringList lines; +#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) + lines = text.split("\n", Qt::SkipEmptyParts); +#else + lines = text.split("\n", QString::SkipEmptyParts); +#endif + for (int i = 0; i < lines.size(); i++) { + QString line = lines.at(i); + QJsonParseError error; + QJsonDocument json = QJsonDocument::fromJson(line.toUtf8(), &error); + if (json.isNull() || !json.isObject()) { + continue; + } + QJsonObject par = json.object(); + QJsonObject data = par["data"].toObject(); + if (!data.contains("exitcode")) { + continue; + } + QString advice = data["advice"].toString(); + QString message = data["message"].toString(); + QString name = data["name"].toString(); + QString url = data["url"].toString(); + QString issues = data["issues"].toString(); + + m_errorMessage->error(name, advice, message, url, issues); + break; + } +} + +void Operator::cancelCommand() +{ + m_commandRunner->stopCommand(); +} + +QString Operator::selectedClusterName() +{ + if (m_isBasicView) { + return "minikube"; + } + return m_advancedView->selectedClusterName(); +} + +Cluster Operator::selectedCluster() +{ + QString clusterName = selectedClusterName(); + if (clusterName.isEmpty()) { + return Cluster(); + } + ClusterList clusters = m_clusterList; + ClusterHash clusterHash; + for (int i = 0; i < clusters.size(); i++) { + Cluster cluster = clusters.at(i); + clusterHash[cluster.name()] = cluster; + } + return clusterHash[clusterName]; +} + +static QString minikubePath() +{ + QString minikubePath = QStandardPaths::findExecutable("minikube"); + if (!minikubePath.isEmpty()) { + return minikubePath; + } + QStringList path = { "/usr/local/bin" }; + return QStandardPaths::findExecutable("minikube", path); +} + +void Operator::sshConsole() +{ + QString program = minikubePath(); + QString commandArgs = QString("ssh -p %1").arg(selectedClusterName()); + QString command = QString("%1 %2").arg(program, commandArgs); +#ifndef QT_NO_TERMWIDGET + QMainWindow *mainWindow = new QMainWindow(); + int startnow = 0; // set shell program first + + QTermWidget *console = new QTermWidget(startnow); + + QFont font = QApplication::font(); + font.setFamily("Monospace"); + font.setPointSize(10); + + console->setTerminalFont(font); + console->setColorScheme("Tango"); + console->setShellProgram(program); + console->setArgs({ commandArgs }); + console->startShellProgram(); + + QObject::connect(console, SIGNAL(finished()), mainWindow, SLOT(close())); + + mainWindow->setWindowTitle(nameLabel->text()); + mainWindow->resize(800, 400); + mainWindow->setCentralWidget(console); + mainWindow->show(); +#elif __APPLE__ + QStringList arguments = { "-e", "tell app \"Terminal\"", + "-e", "do script \"" + command + "\"", + "-e", "activate", + "-e", "end tell" }; + m_commandRunner->executeCommand("/usr/bin/osascript", arguments); +#else + QString terminal = qEnvironmentVariable("TERMINAL"); + if (terminal.isEmpty()) { + terminal = "x-terminal-emulator"; + if (QStandardPaths::findExecutable(terminal).isEmpty()) { + terminal = "xterm"; + } + } + + m_commandRunner->executeCommand(QStandardPaths::findExecutable(terminal), { "-e", command }); +#endif +} + +void Operator::dockerEnv() +{ + QString program = minikubePath(); + QString commandArgs = QString("$(%1 -p %2 docker-env)").arg(program, selectedClusterName()); + QString command = QString("eval %1").arg(commandArgs); +#ifndef QT_NO_TERMWIDGET + QMainWindow *mainWindow = new QMainWindow(); + int startnow = 0; // set shell program first + + QTermWidget *console = new QTermWidget(startnow); + + QFont font = QApplication::font(); + font.setFamily("Monospace"); + font.setPointSize(10); + + console->setTerminalFont(font); + console->setColorScheme("Tango"); + console->setShellProgram("eval"); + console->setArgs({ commandArgs }); + console->startShellProgram(); + + QObject::connect(console, SIGNAL(finished()), mainWindow, SLOT(close())); + + mainWindow->setWindowTitle(nameLabel->text()); + mainWindow->resize(800, 400); + mainWindow->setCentralWidget(console); + mainWindow->show(); +#elif __APPLE__ + QStringList arguments = { "-e", "tell app \"Terminal\"", + "-e", "do script \"" + command + "\"", + "-e", "activate", + "-e", "end tell" }; + m_commandRunner->executeCommand("/usr/bin/osascript", arguments); +#else + QString terminal = qEnvironmentVariable("TERMINAL"); + if (terminal.isEmpty()) { + terminal = "x-terminal-emulator"; + if (QStandardPaths::findExecutable(terminal).isEmpty()) { + terminal = "xterm"; + } + } + + m_commandRunner->executeCommand(QStandardPaths::findExecutable(terminal), { "-e", command }); +#endif +} + +void Operator::dashboardBrowser() +{ + dashboardClose(); + + QString program = minikubePath(); + QProcess *process = new QProcess(this); + QStringList arguments = { "dashboard", "-p", selectedClusterName() }; + process->start(program, arguments); + + dashboardProcess = process; + dashboardProcess->waitForStarted(); +} + +void Operator::dashboardClose() +{ + if (dashboardProcess) { + dashboardProcess->terminate(); + dashboardProcess->waitForFinished(); + } +} diff --git a/operator.h b/operator.h new file mode 100644 index 0000000..1a48712 --- /dev/null +++ b/operator.h @@ -0,0 +1,90 @@ +/* +Copyright 2022 The Kubernetes Authors All rights reserved. + +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. +*/ + +#ifndef OPERATOR_H +#define OPERATOR_H + +#include "advancedview.h" +#include "basicview.h" +#include "cluster.h" +#include "commandrunner.h" +#include "errormessage.h" +#include "progresswindow.h" +#include "tray.h" +#include "hyperkit.h" +#include "updater.h" + +#include + +class Operator : public QObject +{ + Q_OBJECT + +public: + Operator(AdvancedView *advancedView, BasicView *basicView, CommandRunner *commandRunner, + ErrorMessage *errorMessage, ProgressWindow *progressWindow, Tray *tray, + HyperKit *hyperKit, Updater *updater, QStackedWidget *stackedWidget, QDialog *parent); + +public slots: + void startMinikube(); + void stopMinikube(); + void pauseOrUnpauseMinikube(); + void deleteMinikube(); + +private slots: + void commandStarting(); + void commandEnding(); + void commandOutput(QString text); + void commandError(QStringList args, QString text); + void cancelCommand(); + void toBasicView(); + void toAdvancedView(); + void createCluster(QStringList args); + void updateButtons(); + void clustersReceived(ClusterList clusterList); + void startCommandStarting(); + +private: + QStringList getCurrentClusterFlags(); + void updateClusters(); + QString selectedClusterName(); + Cluster selectedCluster(); + void sshConsole(); + void dockerEnv(); + void dashboardBrowser(); + void dashboardClose(); + void pauseMinikube(); + void unpauseMinikube(); + void restoreWindow(); + void hideWindow(); + void disableButtons(); + + AdvancedView *m_advancedView; + BasicView *m_basicView; + CommandRunner *m_commandRunner; + ErrorMessage *m_errorMessage; + ProgressWindow *m_progressWindow; + ClusterList m_clusterList; + Tray *m_tray; + HyperKit *m_hyperKit; + Updater *m_updater; + bool m_isBasicView; + QProcess *dashboardProcess; + QStackedWidget *m_stackedWidget; + QDialog *m_parent; +}; + +#endif // OPERATOR_H diff --git a/progresswindow.cpp b/progresswindow.cpp new file mode 100644 index 0000000..f72c0c1 --- /dev/null +++ b/progresswindow.cpp @@ -0,0 +1,81 @@ +/* +Copyright 2022 The Kubernetes Authors All rights reserved. + +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. +*/ + +#include "progresswindow.h" + +#include +#include + +ProgressWindow::ProgressWindow(QWidget *parent, QIcon icon) +{ + m_icon = icon; + + m_dialog = new QDialog(parent); + m_dialog->setWindowIcon(m_icon); + m_dialog->resize(300, 150); + m_dialog->setWindowFlags(Qt::FramelessWindowHint); + m_dialog->setModal(true); + + QVBoxLayout form(m_dialog); + + m_text = new QLabel(); + m_text->setWordWrap(true); + form.addWidget(m_text); + + m_progressBar = new QProgressBar(); + form.addWidget(m_progressBar); + + m_cancelButton = new QPushButton(); + m_cancelButton->setText(tr("Cancel")); + connect(m_cancelButton, &QAbstractButton::clicked, this, &ProgressWindow::cancel); + form.addWidget(m_cancelButton); + + // if the dialog isn't opened now it breaks formatting + m_dialog->open(); + m_dialog->hide(); +} + +void ProgressWindow::setBarMaximum(int max) +{ + m_progressBar->setMaximum(max); +} + +void ProgressWindow::setBarValue(int value) +{ + m_progressBar->setValue(value); +} + +void ProgressWindow::setText(QString text) +{ + m_text->setText(text); +} + +void ProgressWindow::show() +{ + m_dialog->open(); +} + +void ProgressWindow::cancel() +{ + done(); + emit cancelled(); +} + +void ProgressWindow::done() +{ + m_dialog->hide(); + m_progressBar->setValue(0); +} diff --git a/progresswindow.h b/progresswindow.h new file mode 100644 index 0000000..2ffec96 --- /dev/null +++ b/progresswindow.h @@ -0,0 +1,54 @@ +/* +Copyright 2022 The Kubernetes Authors All rights reserved. + +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. +*/ + +#ifndef PROGRESSWINDOW_H +#define PROGRESSWINDOW_H + +#include +#include +#include +#include +#include +#include +#include + +class ProgressWindow : public QObject +{ + Q_OBJECT + +public: + explicit ProgressWindow(QWidget *parent, QIcon icon); + + void setBarMaximum(int max); + void setBarValue(int value); + void setText(QString text); + void show(); + void done(); + +signals: + void cancelled(); + +private: + void cancel(); + + QDialog *m_dialog; + QLabel *m_text; + QProgressBar *m_progressBar; + QPushButton *m_cancelButton; + QIcon m_icon; +}; + +#endif // PROGRESSWINDOW_H diff --git a/releases.json b/releases.json new file mode 100644 index 0000000..7b4698c --- /dev/null +++ b/releases.json @@ -0,0 +1,5 @@ +[ + { + "name":"0.0.0" + } +] diff --git a/tray.cpp b/tray.cpp new file mode 100644 index 0000000..3405f71 --- /dev/null +++ b/tray.cpp @@ -0,0 +1,132 @@ +/* +Copyright 2022 The Kubernetes Authors All rights reserved. + +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. +*/ + +#include "tray.h" + +#include +#include +#include + +Tray::Tray(QIcon icon) +{ + m_icon = icon; + + trayIconMenu = new QMenu(); + trayIcon = new QSystemTrayIcon(this); + + connect(trayIcon, &QSystemTrayIcon::activated, this, &Tray::iconActivated); + + minimizeAction = new QAction(tr("Mi&nimize"), this); + connect(minimizeAction, &QAction::triggered, this, &Tray::hideWindow); + + restoreAction = new QAction(tr("&Restore"), this); + connect(restoreAction, &QAction::triggered, this, &Tray::restoreWindow); + + quitAction = new QAction(tr("&Quit"), this); + connect(quitAction, &QAction::triggered, qApp, &QCoreApplication::quit); + + startAction = new QAction(tr("Start"), this); + connect(startAction, &QAction::triggered, this, &Tray::start); + + pauseAction = new QAction(tr("Pause"), this); + connect(pauseAction, &QAction::triggered, this, &Tray::pauseOrUnpause); + + stopAction = new QAction(tr("Stop"), this); + connect(stopAction, &QAction::triggered, this, &Tray::stop); + + statusAction = new QAction(tr("Status:"), this); + statusAction->setEnabled(false); + + trayIconMenu->addAction(statusAction); + trayIconMenu->addSeparator(); + trayIconMenu->addAction(startAction); + trayIconMenu->addAction(pauseAction); + trayIconMenu->addAction(stopAction); + trayIconMenu->addSeparator(); + trayIconMenu->addAction(minimizeAction); + trayIconMenu->addAction(restoreAction); + trayIconMenu->addSeparator(); + trayIconMenu->addAction(quitAction); + + trayIcon->setContextMenu(trayIconMenu); + trayIcon->setIcon(m_icon); + trayIcon->show(); +} + +void Tray::iconActivated(QSystemTrayIcon::ActivationReason reason) +{ + switch (reason) { + case QSystemTrayIcon::Trigger: + case QSystemTrayIcon::DoubleClick: + emit restoreWindow(); + break; + default:; + } +} + +void Tray::updateStatus(Cluster cluster) +{ + QString status = cluster.status(); + if (status.isEmpty()) { + status = "Stopped"; + } + statusAction->setText("Status: " + status); +} + +bool Tray::isVisible() +{ + return trayIcon->isVisible(); +} + +void Tray::setVisible(bool visible) +{ + minimizeAction->setEnabled(visible); + restoreAction->setEnabled(!visible); +} + +static QString getPauseLabel(bool isPaused) +{ + if (isPaused) { + return "Unpause"; + } + return "Pause"; +} + +static QString getStartLabel(bool isRunning) +{ + if (isRunning) { + return "Restart"; + } + return "Start"; +} + +void Tray::updateTrayActions(Cluster cluster) +{ + startAction->setEnabled(true); + bool isRunning = cluster.status() == "Running"; + bool isPaused = cluster.status() == "Paused"; + pauseAction->setEnabled(isRunning || isPaused); + stopAction->setEnabled(isRunning || isPaused); + pauseAction->setText(getPauseLabel(isPaused)); + startAction->setText(getStartLabel(isRunning)); +} + +void Tray::disableActions() +{ + startAction->setEnabled(false); + stopAction->setEnabled(false); + pauseAction->setEnabled(false); +} diff --git a/tray.h b/tray.h new file mode 100644 index 0000000..9c7c794 --- /dev/null +++ b/tray.h @@ -0,0 +1,61 @@ +/* +Copyright 2022 The Kubernetes Authors All rights reserved. + +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. +*/ + +#ifndef TRAY_H +#define TRAY_H + +#include "cluster.h" + +#include +#include + +class Tray : public QObject +{ + Q_OBJECT + +public: + explicit Tray(QIcon icon); + bool isVisible(); + void setVisible(bool visible); + void updateStatus(Cluster cluster); + void updateTrayActions(Cluster cluster); + void disableActions(); + +signals: + void restoreWindow(); + void showWindow(); + void hideWindow(); + void start(); + void stop(); + void pauseOrUnpause(); + +private: + void createTrayIcon(); + void createActions(); + void iconActivated(QSystemTrayIcon::ActivationReason reason); + QAction *minimizeAction; + QAction *restoreAction; + QAction *quitAction; + QAction *startAction; + QAction *pauseAction; + QAction *stopAction; + QAction *statusAction; + QSystemTrayIcon *trayIcon; + QMenu *trayIconMenu; + QIcon m_icon; +}; + +#endif // TRAY_H diff --git a/updater.cpp b/updater.cpp new file mode 100644 index 0000000..9f98581 --- /dev/null +++ b/updater.cpp @@ -0,0 +1,133 @@ +/* +Copyright 2022 The Kubernetes Authors All rights reserved. + +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. +*/ + +#include "updater.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +Updater::Updater(QVersionNumber version, QIcon icon) +{ + m_version = version; + m_icon = icon; +} + +static bool checkedForUpdateRecently() +{ + QString filePath = QStandardPaths::locate(QStandardPaths::HomeLocation, + "/.minikube-gui/last_update_check"); + if (filePath == "") { + return false; + } + QFile file(filePath); + if (!file.open(QIODevice::ReadOnly)) { + return false; + } + QTextStream in(&file); + QString line = in.readLine(); + QDateTime nextCheck = QDateTime::fromString(line).addSecs(60 * 60 * 24); + QDateTime now = QDateTime::currentDateTime(); + return nextCheck > now; +} + +static void logUpdateCheck() +{ + QDir dir = QDir(QDir::homePath() + "/.minikube-gui"); + if (!dir.exists()) { + dir.mkpath("."); + } + QString filePath = dir.filePath("last_update_check"); + QFile file(filePath); + if (!file.open(QIODevice::WriteOnly)) { + return; + } + QTextStream stream(&file); + stream << QDateTime::currentDateTime().toString() << "\n"; +} + +void Updater::checkForUpdates() +{ + if (checkedForUpdateRecently()) { + return; + } + logUpdateCheck(); + QString releases = getRequest("https://storage.googleapis.com/minikube-gui/releases.json"); + QJsonObject latestRelease = + QJsonDocument::fromJson(releases.toUtf8()).array().first().toObject(); + QString latestReleaseVersion = latestRelease["name"].toString(); + QVersionNumber latestReleaseVersionNumber = QVersionNumber::fromString(latestReleaseVersion); + if (m_version >= latestReleaseVersionNumber) { + return; + } + QJsonObject links = latestRelease["links"].toObject(); + QString key; +#if __linux__ + key = "linux"; +#elif __APPLE__ + key = "darwin"; +#else + key = "windows"; +#endif + QString link = links[key].toString(); + notifyUpdate(latestReleaseVersion, link); +} + +void Updater::notifyUpdate(QString latest, QString link) +{ + QDialog dialog; + dialog.setWindowTitle(tr("minikube GUI Update Available")); + dialog.setWindowIcon(m_icon); + dialog.setModal(true); + QFormLayout form(&dialog); + QLabel *msgLabel = new QLabel(); + msgLabel->setText("Version " + latest + + " of minikube GUI is now available!\n\nDownload the update from:"); + form.addWidget(msgLabel); + QLabel *linkLabel = new QLabel(); + linkLabel->setOpenExternalLinks(true); + linkLabel->setText("" + link + ""); + form.addWidget(linkLabel); + QDialogButtonBox buttonBox(Qt::Horizontal, &dialog); + buttonBox.addButton(QString(tr("OK")), QDialogButtonBox::AcceptRole); + connect(&buttonBox, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); + form.addRow(&buttonBox); + dialog.exec(); +} + +QString Updater::getRequest(QString url) +{ + QNetworkAccessManager *manager = new QNetworkAccessManager(); + QObject::connect(manager, &QNetworkAccessManager::finished, this, [=](QNetworkReply *reply) { + if (reply->error()) { + qDebug() << reply->errorString(); + } + }); + QNetworkReply *resp = manager->get(QNetworkRequest(QUrl(url))); + QEventLoop loop; + connect(resp, &QNetworkReply::finished, &loop, &QEventLoop::quit); + loop.exec(); + return resp->readAll(); +} diff --git a/updater.h b/updater.h new file mode 100644 index 0000000..ee0de93 --- /dev/null +++ b/updater.h @@ -0,0 +1,39 @@ +/* +Copyright 2022 The Kubernetes Authors All rights reserved. + +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. +*/ + +#ifndef UPDATER_H +#define UPDATER_H + +#include +#include +#include + +class Updater : public QObject +{ + Q_OBJECT + +public: + explicit Updater(QVersionNumber version, QIcon icon); + void checkForUpdates(); + +private: + void notifyUpdate(QString latest, QString link); + QString getRequest(QString url); + QVersionNumber m_version; + QIcon m_icon; +}; + +#endif // UPDATER_H diff --git a/window.cpp b/window.cpp new file mode 100644 index 0000000..0ed58ff --- /dev/null +++ b/window.cpp @@ -0,0 +1,152 @@ +/* +Copyright 2022 The Kubernetes Authors All rights reserved. + +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. +*/ + +#include "window.h" + +#ifndef QT_NO_SYSTEMTRAYICON + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef QT_NO_TERMWIDGET +#include +#include +#include "qtermwidget.h" +#endif + +const QVersionNumber version = QVersionNumber::fromString("0.0.1"); + +Window::Window() +{ + trayIconIcon = new QIcon(":/images/minikube.png"); + checkForMinikube(); + + stackedWidget = new QStackedWidget; + logger = new Logger(); + commandRunner = new CommandRunner(this, logger); + basicView = new BasicView(); + advancedView = new AdvancedView(*trayIconIcon); + errorMessage = new ErrorMessage(this, *trayIconIcon); + progressWindow = new ProgressWindow(this, *trayIconIcon); + tray = new Tray(*trayIconIcon); + hyperKit = new HyperKit(*trayIconIcon); + updater = new Updater(version, *trayIconIcon); + + op = new Operator(advancedView, basicView, commandRunner, errorMessage, progressWindow, tray, + hyperKit, updater, stackedWidget, this); + + stackedWidget->addWidget(basicView->basicView); + stackedWidget->addWidget(advancedView->advancedView); + layout = new QVBoxLayout; + layout->addWidget(stackedWidget); + setLayout(layout); + resize(200, 300); + setWindowTitle(tr("minikube")); + setWindowIcon(*trayIconIcon); +} + +void Window::setVisible(bool visible) +{ + tray->setVisible(visible); + QDialog::setVisible(visible); +} + +void Window::closeEvent(QCloseEvent *event) +{ +#if __APPLE__ + if (!event->spontaneous() || !isVisible()) { + return; + } +#endif + if (tray->isVisible()) { + QMessageBox::information(this, tr("minikube"), + tr("minikube will minimize to the " + "system tray. To terminate the program, " + "choose Quit in the context menu " + "of the system tray entry.")); + hide(); + event->ignore(); + } +} + +static QString minikubePath() +{ + QString minikubePath = QStandardPaths::findExecutable("minikube"); + if (!minikubePath.isEmpty()) { + return minikubePath; + } + QStringList path = { "/usr/local/bin" }; + return QStandardPaths::findExecutable("minikube", path); +} + +void Window::checkForMinikube() +{ + QString program = minikubePath(); + if (!program.isEmpty()) { + return; + } + + QDialog dialog; + dialog.setWindowTitle(tr("minikube")); + dialog.setWindowIcon(*trayIconIcon); + dialog.setModal(true); + QFormLayout form(&dialog); + QLabel *message = new QLabel(this); + message->setText("minikube was not found on the path.\nPlease follow the install instructions " + "below to install minikube first.\n"); + form.addWidget(message); + QLabel *link = new QLabel(this); + link->setOpenExternalLinks(true); + link->setText("https://minikube.sigs.k8s.io/" + "docs/start/"); + form.addWidget(link); + QDialogButtonBox buttonBox(Qt::Horizontal, &dialog); + buttonBox.addButton(QString(tr("OK")), QDialogButtonBox::AcceptRole); + connect(&buttonBox, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); + form.addRow(&buttonBox); + dialog.exec(); + exit(EXIT_FAILURE); +} + +#endif diff --git a/window.h b/window.h new file mode 100644 index 0000000..8feff4a --- /dev/null +++ b/window.h @@ -0,0 +1,96 @@ +/* +Copyright 2022 The Kubernetes Authors All rights reserved. + +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. +*/ + +#ifndef WINDOW_H +#define WINDOW_H + +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef QT_NO_SYSTEMTRAYICON + +#include + +QT_BEGIN_NAMESPACE +class QAction; +class QCheckBox; +class QComboBox; +class QGroupBox; +class QIcon; +class QLabel; +class QLineEdit; +class QMenu; +class QPushButton; +class QSpinBox; +class QTextEdit; +class QTableView; +class QProcess; +QT_END_NAMESPACE + +#include "basicview.h" +#include "advancedview.h" +#include "progresswindow.h" +#include "operator.h" +#include "errormessage.h" +#include "tray.h" +#include "hyperkit.h" +#include "updater.h" +#include "logger.h" + +class Window : public QDialog +{ + Q_OBJECT + +public: + Window(); + + void setVisible(bool visible) override; + void restoreWindow(); + +protected: + void closeEvent(QCloseEvent *event) override; + +private: + QIcon *trayIconIcon; + + void checkForMinikube(); + QStackedWidget *stackedWidget; + void checkForUpdates(); + QString getRequest(QString url); + void notifyUpdate(QString latest, QString link); + + BasicView *basicView; + AdvancedView *advancedView; + Operator *op; + CommandRunner *commandRunner; + ErrorMessage *errorMessage; + ProgressWindow *progressWindow; + Tray *tray; + HyperKit *hyperKit; + Updater *updater; + QVBoxLayout *layout; + Logger *logger; +}; + +#endif // QT_NO_SYSTEMTRAYICON + +#endif